editor.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498
  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 && !url.startsWith('data:') ? `${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 getRootPrefix(url) {
  108. const u = new URL(url, window.location.href);
  109. return u.origin;
  110. }
  111. function removeDotDotSlash(href) {
  112. // assumes a well formed URL. In other words: 'https://..//foo.html" is a bad URL and this code would fail.
  113. const url = new URL(href, window.location.href);
  114. const parts = url.pathname.split('/');
  115. for (;;) {
  116. const dotDotNdx = parts.indexOf('..');
  117. if (dotDotNdx < 0) {
  118. break;
  119. }
  120. parts.splice(dotDotNdx - 1, 2);
  121. }
  122. url.pathname = parts.join('/');
  123. return url.toString();
  124. }
  125. function forEachHTMLPart(fn) {
  126. Object.keys(htmlParts).forEach(function(name, ndx) {
  127. const info = htmlParts[name];
  128. fn(info, ndx, name);
  129. });
  130. }
  131. function getHTMLPart(re, obj, tag) {
  132. let part = '';
  133. obj.html = obj.html.replace(re, function(p0, p1) {
  134. part = p1;
  135. return tag;
  136. });
  137. return part.replace(/\s*/, '');
  138. }
  139. // doesn't handle multi-line comments or comments with { or } in them
  140. function formatCSS(css) {
  141. let indent = '';
  142. return css.split('\n').map((line) => {
  143. let currIndent = indent;
  144. if (line.includes('{')) {
  145. indent = indent + ' ';
  146. } else if (line.includes('}')) {
  147. indent = indent.substring(0, indent.length - 2);
  148. currIndent = indent;
  149. }
  150. return `${currIndent}${line.trim()}`;
  151. }).join('\n');
  152. }
  153. async function getScript(url, scriptInfos) {
  154. // check it's an example script, not some other lib
  155. if (!scriptInfos[url].source) {
  156. const source = await getHTML(url);
  157. const fixedSource = fixSourceLinks(url, source);
  158. const {text} = await getWorkerScripts(fixedSource, url, scriptInfos);
  159. scriptInfos[url].source = text;
  160. }
  161. }
  162. /**
  163. * @typedef {Object} ScriptInfo
  164. * @property {string} fqURL The original fully qualified URL
  165. * @property {ScriptInfo[]} deps Array of other ScriptInfos this is script dependant on
  166. * @property {boolean} isWorker True if this script came from `new Worker('someurl')` vs `import` or `importScripts`
  167. * @property {string} blobUrl The blobUrl for this script if one has been made
  168. * @property {number} blobGenerationId Used to not visit things twice while recursing.
  169. * @property {string} source The source as extracted. Updated from editor by getSourcesFromEditor
  170. * @property {string} munged The source after urls have been replaced with blob urls etc... (the text send to new Blob)
  171. */
  172. async function getWorkerScripts(text, baseUrl, scriptInfos = {}) {
  173. const parentScriptInfo = scriptInfos[baseUrl];
  174. const workerRE = /(new\s+Worker\s*\(\s*)('|")(.*?)('|")/g;
  175. const importScriptsRE = /(importScripts\s*\(\s*)('|")(.*?)('|")/g;
  176. const importRE = /(import.*?)(?!'three')('|")(.*?)('|")/g;
  177. const newScripts = [];
  178. const slashRE = /\/manual\/examples\/[^/]+$/;
  179. function replaceWithUUID(match, prefix, quote, url) {
  180. const fqURL = getFQUrl(url, baseUrl);
  181. if (!slashRE.test(fqURL)) {
  182. return match.toString();
  183. }
  184. if (!scriptInfos[url]) {
  185. scriptInfos[fqURL] = {
  186. fqURL,
  187. deps: [],
  188. isWorker: prefix.indexOf('Worker') >= 0,
  189. };
  190. newScripts.push(fqURL);
  191. }
  192. parentScriptInfo.deps.push(scriptInfos[fqURL]);
  193. return `${prefix}${quote}${fqURL}${quote}`;
  194. }
  195. function replaceWithUUIDModule(match, prefix, quote, url) {
  196. // modules are either relative, fully qualified, or a module name
  197. // Skip it if it's a module name
  198. return (url.startsWith('.') || url.includes('://'))
  199. ? replaceWithUUID(match, prefix, quote, url)
  200. : match.toString();
  201. }
  202. text = text.replace(workerRE, replaceWithUUID);
  203. text = text.replace(importScriptsRE, replaceWithUUID);
  204. text = text.replace(importRE, replaceWithUUIDModule);
  205. await Promise.all(newScripts.map((url) => {
  206. return getScript(url, scriptInfos);
  207. }));
  208. return {text, scriptInfos};
  209. }
  210. // hack: scriptInfo is undefined for html and css
  211. // should try to include html and css in scriptInfos
  212. function addSource(type, name, source, scriptInfo) {
  213. htmlParts[type].sources.push({source, name, scriptInfo});
  214. }
  215. function safeStr(s) {
  216. return s === undefined ? '' : s;
  217. }
  218. async function parseHTML(url, html) {
  219. html = fixSourceLinks(url, html);
  220. html = html.replace(/<div class="description">[^]*?<\/div>/, '');
  221. const styleRE = /<style>([^]*?)<\/style>/i;
  222. const titleRE = /<title>([^]*?)<\/title>/i;
  223. const bodyRE = /<body>([^]*?)<\/body>/i;
  224. const inlineScriptRE = /<script>([^]*?)<\/script>/i;
  225. const inlineModuleScriptRE = /<script type="module">([^]*?)<\/script>/i;
  226. const externalScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script\s+([^>]*?)(type="module"\s+)?src\s*=\s*"(.*?)"(.*?)>\s*<\/script>/ig;
  227. const dataScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script([^>]*?type="(?!module).*?".*?)>([^]*?)<\/script>/ig;
  228. const cssLinkRE = /<link ([^>]+?)>/g;
  229. const isCSSLinkRE = /type="text\/css"|rel="stylesheet"/;
  230. const hrefRE = /href="([^"]+)"/;
  231. const obj = { html: html };
  232. addSource('css', 'css', formatCSS(fixCSSLinks(url, getHTMLPart(styleRE, obj, '<style>\n${css}</style>'))));
  233. addSource('html', 'html', getHTMLPart(bodyRE, obj, '<body>${html}</body>'));
  234. const rootScript = getHTMLPart(inlineScriptRE, obj, '<script>${js}</script>') ||
  235. getHTMLPart(inlineModuleScriptRE, obj, '<script type="module">${js}</script>');
  236. html = obj.html;
  237. const fqURL = getFQUrl(url);
  238. /** @type Object<string, SourceInfo> */
  239. const scriptInfos = {};
  240. g.rootScriptInfo = {
  241. fqURL,
  242. deps: [],
  243. source: rootScript,
  244. };
  245. scriptInfos[fqURL] = g.rootScriptInfo;
  246. const {text} = await getWorkerScripts(rootScript, fqURL, scriptInfos);
  247. g.rootScriptInfo.source = text;
  248. g.scriptInfos = scriptInfos;
  249. for (const [fqURL, scriptInfo] of Object.entries(scriptInfos)) {
  250. addSource('js', basename(fqURL), scriptInfo.source, scriptInfo);
  251. }
  252. const tm = titleRE.exec(html);
  253. if (tm) {
  254. g.title = tm[1];
  255. }
  256. const kScript = 'script';
  257. const scripts = [];
  258. html = html.replace(externalScriptRE, function(p0, p1, p2, type, p3, p4) {
  259. p1 = p1 || '';
  260. scripts.push(`${p1}<${kScript} ${p2}${safeStr(type)}src="${p3}"${p4}></${kScript}>`);
  261. return '';
  262. });
  263. const prefix = getPrefix(url);
  264. const rootPrefix = getRootPrefix(url);
  265. function addCorrectPrefix(href) {
  266. return (href.startsWith('/'))
  267. ? `${rootPrefix}${href}`
  268. : removeDotDotSlash((`${prefix}/${href}`).replace(/\/.\//g, '/'));
  269. }
  270. function addPrefix(url) {
  271. return url.indexOf('://') < 0 && !url.startsWith('data:') && url[0] !== '?'
  272. ? removeDotDotSlash(addCorrectPrefix(url))
  273. : url;
  274. }
  275. const importMapRE = /type\s*=["']importmap["']/;
  276. const dataScripts = [];
  277. html = html.replace(dataScriptRE, function(p0, blockComments, scriptTagAttrs, content) {
  278. blockComments = blockComments || '';
  279. if (importMapRE.test(scriptTagAttrs)) {
  280. const imap = JSON.parse(content);
  281. const imports = imap.imports;
  282. if (imports) {
  283. for (let [k, url] of Object.entries(imports)) {
  284. if (url.indexOf('://') < 0 && !url.startsWith('data:')) {
  285. imports[k] = addPrefix(url);
  286. }
  287. }
  288. }
  289. content = JSON.stringify(imap, null, '\t');
  290. }
  291. dataScripts.push(`${blockComments}<${kScript} ${scriptTagAttrs}>${content}</${kScript}>`);
  292. return '';
  293. });
  294. htmlParts.html.sources[0].source += dataScripts.join('\n');
  295. htmlParts.html.sources[0].source += scripts.join('\n');
  296. // add style section if there is non
  297. if (html.indexOf('${css}') < 0) {
  298. html = html.replace('</head>', '<style>\n${css}</style>\n</head>');
  299. }
  300. // add hackedparams section.
  301. // We need a way to pass parameters to a blob. Normally they'd be passed as
  302. // query params but that only works in Firefox >:(
  303. html = html.replace('</head>', '<script id="hackedparams">window.hackedParams = ${hackedParams}\n</script>\n</head>');
  304. html = extraHTMLParsing(html, htmlParts);
  305. let links = '';
  306. html = html.replace(cssLinkRE, function(p0, p1) {
  307. if (isCSSLinkRE.test(p1)) {
  308. const m = hrefRE.exec(p1);
  309. if (m) {
  310. links += `@import url("${m[1]}");\n`;
  311. }
  312. return '';
  313. } else {
  314. return p0;
  315. }
  316. });
  317. htmlParts.css.sources[0].source = links + htmlParts.css.sources[0].source;
  318. g.html = html;
  319. }
  320. async function main() {
  321. const query = getQuery();
  322. g.url = getFQUrl(query.url);
  323. g.query = getSearch(g.url);
  324. let html;
  325. try {
  326. html = await getHTML(query.url);
  327. } catch (err) {
  328. console.log(err); // eslint-disable-line
  329. return;
  330. }
  331. await parseHTML(query.url, html);
  332. setupEditor();
  333. if (query.startPane) {
  334. const button = document.querySelector('.button-' + query.startPane);
  335. toggleSourcePane(button);
  336. }
  337. }
  338. function getJavaScriptBlob(source) {
  339. const blob = new Blob([source], {type: 'application/javascript'});
  340. return URL.createObjectURL(blob);
  341. }
  342. let blobGeneration = 0;
  343. function makeBlobURLsForSources(scriptInfo) {
  344. ++blobGeneration;
  345. function makeBlobURLForSourcesImpl(scriptInfo) {
  346. if (scriptInfo.blobGenerationId !== blobGeneration) {
  347. scriptInfo.blobGenerationId = blobGeneration;
  348. if (scriptInfo.blobUrl) {
  349. URL.revokeObjectURL(scriptInfo.blobUrl);
  350. }
  351. scriptInfo.deps.forEach(makeBlobURLForSourcesImpl);
  352. let text = scriptInfo.source;
  353. scriptInfo.deps.forEach((depScriptInfo) => {
  354. text = text.split(depScriptInfo.fqURL).join(depScriptInfo.blobUrl);
  355. });
  356. scriptInfo.numLinesBeforeScript = 0;
  357. if (scriptInfo.isWorker) {
  358. const extra = `self.lessonSettings = ${JSON.stringify(lessonSettings)};
  359. import '${dirname(scriptInfo.fqURL)}/resources/webgl-debug-helper.js';
  360. import '${dirname(scriptInfo.fqURL)}/resources/lessons-worker-helper.js';`;
  361. scriptInfo.numLinesBeforeScript = extra.split('\n').length;
  362. text = `${extra}\n${text}`;
  363. }
  364. scriptInfo.blobUrl = getJavaScriptBlob(text);
  365. scriptInfo.munged = text;
  366. }
  367. }
  368. makeBlobURLForSourcesImpl(scriptInfo);
  369. }
  370. function getSourceBlob(htmlParts) {
  371. g.rootScriptInfo.source = htmlParts.js;
  372. makeBlobURLsForSources(g.rootScriptInfo);
  373. const dname = dirname(g.url);
  374. // HACK! for webgl-2d-vs... those examples are not in /webgl they're in /webgl/resources
  375. // We basically assume url is https://foo/base/example.html so there will be 4 slashes
  376. // If the path is longer than then we need '../' to back up so prefix works below
  377. const prefix = dname; //`${dname}${dname.split('/').slice(4).map(() => '/..').join('')}`;
  378. let source = g.html;
  379. source = source.replace('${hackedParams}', JSON.stringify(g.query));
  380. source = source.replace('${html}', htmlParts.html);
  381. source = source.replace('${css}', htmlParts.css);
  382. source = source.replace('${js}', g.rootScriptInfo.munged); //htmlParts.js);
  383. source = source.replace('<head>', `<head>
  384. <link rel="stylesheet" href="${prefix}/resources/lesson-helper.css" type="text/css">
  385. <script match="false">self.lessonSettings = ${JSON.stringify(lessonSettings)}</script>`);
  386. source = source.replace('</head>', `<script src="${prefix}/resources/webgl-debug-helper.js"></script>
  387. <script src="${prefix}/resources/lessons-helper.js"></script>
  388. </head>`);
  389. const scriptNdx = source.search(/<script(\s+type="module"\s*)?>/);
  390. g.rootScriptInfo.numLinesBeforeScript = (source.substring(0, scriptNdx).match(/\n/g) || []).length;
  391. const blob = new Blob([source], {type: 'text/html'});
  392. // This seems hacky. We are combining html/css/js into one html blob but we already made
  393. // a blob for the JS so let's replace that blob. That means it will get auto-released when script blobs
  394. // are regenerated. It also means error reporting will work
  395. const blobUrl = URL.createObjectURL(blob);
  396. URL.revokeObjectURL(g.rootScriptInfo.blobUrl);
  397. g.rootScriptInfo.blobUrl = blobUrl;
  398. return blobUrl;
  399. }
  400. function getSourcesFromEditor() {
  401. for (const partTypeInfo of Object.values(htmlParts)) {
  402. for (const source of partTypeInfo.sources) {
  403. source.source = source.editor.getValue();
  404. // hack: shouldn't store this twice. Also see other comment,
  405. // should consolidate so scriptInfo is used for css and html
  406. if (source.scriptInfo) {
  407. source.scriptInfo.source = source.source;
  408. }
  409. }
  410. }
  411. }
  412. function getSourceBlobFromEditor() {
  413. getSourcesFromEditor();
  414. return getSourceBlob({
  415. html: htmlParts.html.sources[0].source,
  416. css: htmlParts.css.sources[0].source,
  417. js: htmlParts.js.sources[0].source,
  418. });
  419. }
  420. function getSourceBlobFromOrig() {
  421. return getSourceBlob({
  422. html: htmlParts.html.sources[0].source,
  423. css: htmlParts.css.sources[0].source,
  424. js: htmlParts.js.sources[0].source,
  425. });
  426. }
  427. function dirname(path) {
  428. const ndx = path.lastIndexOf('/');
  429. return path.substring(0, ndx);
  430. }
  431. function basename(path) {
  432. const ndx = path.lastIndexOf('/');
  433. return path.substring(ndx + 1);
  434. }
  435. function resize() {
  436. forEachHTMLPart(function(info) {
  437. info.editors.forEach((editorInfo) => {
  438. editorInfo.editor.layout();
  439. });
  440. });
  441. }
  442. function getScripts(scriptInfo) {
  443. ++blobGeneration;
  444. function getScriptsImpl(scriptInfo) {
  445. const scripts = [];
  446. if (scriptInfo.blobGenerationId !== blobGeneration) {
  447. scriptInfo.blobGenerationId = blobGeneration;
  448. scripts.push(...scriptInfo.deps.map(getScriptsImpl).flat());
  449. let text = scriptInfo.source;
  450. scriptInfo.deps.forEach((depScriptInfo) => {
  451. text = text.split(depScriptInfo.fqURL).join(`worker-${basename(depScriptInfo.fqURL)}`);
  452. });
  453. scripts.push({
  454. name: `worker-${basename(scriptInfo.fqURL)}`,
  455. text,
  456. });
  457. }
  458. return scripts;
  459. }
  460. return getScriptsImpl(scriptInfo);
  461. }
  462. function makeScriptsForWorkers(scriptInfo) {
  463. const scripts = getScripts(scriptInfo);
  464. if (scripts.length === 1) {
  465. return {
  466. js: scripts[0].text,
  467. html: '',
  468. };
  469. }
  470. // scripts[last] = main script
  471. // scripts[last - 1] = worker
  472. const mainScriptInfo = scripts[scripts.length - 1];
  473. const workerScriptInfo = scripts[scripts.length - 2];
  474. const workerName = workerScriptInfo.name;
  475. mainScriptInfo.text = mainScriptInfo.text.split(`'${workerName}'`).join('getWorkerBlob()');
  476. const html = scripts.map((nameText) => {
  477. const {name, text} = nameText;
  478. return `<script id="${name}" type="x-worker">\n${text}\n</script>\n`;
  479. }).join('\n');
  480. const init = `
  481. // ------
  482. // Creates Blobs for the Scripts so things can be self contained for snippets/JSFiddle/Codepen
  483. // even though they are using workers
  484. //
  485. (function() {
  486. const idsToUrls = [];
  487. const scriptElements = [...document.querySelectorAll('script[type=x-worker]')];
  488. for (const scriptElement of scriptElements) {
  489. let text = scriptElement.text;
  490. for (const {id, url} of idsToUrls) {
  491. text = text.split(id).join(url);
  492. }
  493. const blob = new Blob([text], {type: 'application/javascript'});
  494. const url = URL.createObjectURL(blob);
  495. const id = scriptElement.id;
  496. idsToUrls.push({id, url});
  497. }
  498. window.getWorkerBlob = function() {
  499. return idsToUrls.pop().url;
  500. };
  501. import(window.getWorkerBlob());
  502. }());
  503. `;
  504. return {
  505. js: init,
  506. html,
  507. };
  508. }
  509. async function fixHTMLForCodeSite(html) {
  510. html = html.replace(lessonHelperScriptRE, '');
  511. html = html.replace(webglDebugHelperScriptRE, '');
  512. return html;
  513. }
  514. async function openInCodepen() {
  515. const comment = `// ${g.title}
  516. // from ${g.url}
  517. `;
  518. getSourcesFromEditor();
  519. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  520. const code = await fixJSForCodeSite(scripts.js);
  521. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  522. const pen = {
  523. title : g.title,
  524. description : 'from: ' + g.url,
  525. tags : lessonEditorSettings.tags,
  526. editors : '101',
  527. html : scripts.html + html,
  528. css : htmlParts.css.sources[0].source,
  529. js : comment + code,
  530. };
  531. const elem = document.createElement('div');
  532. elem.innerHTML = `
  533. <form method="POST" target="_blank" action="https://codepen.io/pen/define" class="hidden">'
  534. <input type="hidden" name="data">
  535. <input type="submit" />
  536. "</form>"
  537. `;
  538. elem.querySelector('input[name=data]').value = JSON.stringify(pen);
  539. window.frameElement.ownerDocument.body.appendChild(elem);
  540. elem.querySelector('form').submit();
  541. window.frameElement.ownerDocument.body.removeChild(elem);
  542. }
  543. async function openInJSFiddle() {
  544. const comment = `// ${g.title}
  545. // from ${g.url}
  546. `;
  547. getSourcesFromEditor();
  548. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  549. const code = await fixJSForCodeSite(scripts.js);
  550. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  551. const elem = document.createElement('div');
  552. elem.innerHTML = `
  553. <form method="POST" target="_black" action="https://jsfiddle.net/api/mdn/" class="hidden">
  554. <input type="hidden" name="html" />
  555. <input type="hidden" name="css" />
  556. <input type="hidden" name="js" />
  557. <input type="hidden" name="title" />
  558. <input type="hidden" name="wrap" value="b" />
  559. <input type="submit" />
  560. </form>
  561. `;
  562. elem.querySelector('input[name=html]').value = scripts.html + html;
  563. elem.querySelector('input[name=css]').value = htmlParts.css.sources[0].source;
  564. elem.querySelector('input[name=js]').value = comment + code;
  565. elem.querySelector('input[name=title]').value = g.title;
  566. window.frameElement.ownerDocument.body.appendChild(elem);
  567. elem.querySelector('form').submit();
  568. window.frameElement.ownerDocument.body.removeChild(elem);
  569. }
  570. async function openInJSGist() {
  571. const comment = `// ${g.title}
  572. // from ${g.url}
  573. `;
  574. getSourcesFromEditor();
  575. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  576. const code = await fixJSForCodeSite(scripts.js);
  577. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  578. const gist = {
  579. name: g.title,
  580. settings: {},
  581. files: [
  582. { name: 'index.html', content: scripts.html + html, },
  583. { name: 'index.css', content: htmlParts.css.sources[0].source, },
  584. { name: 'index.js', content: comment + code, },
  585. ],
  586. };
  587. window.open('https://jsgist.org/?newGist=true', '_blank');
  588. const send = (e) => {
  589. e.source.postMessage({type: 'newGist', data: gist}, '*');
  590. };
  591. window.addEventListener('message', send, {once: true});
  592. }
  593. /*
  594. <!-- begin snippet: js hide: false console: true babel: false -->
  595. <!-- language: lang-js -->
  596. console.log();
  597. <!-- language: lang-css -->
  598. h1 { color: red; }
  599. <!-- language: lang-html -->
  600. <h1>foo</h1>
  601. <!-- end snippet -->
  602. */
  603. function indent4(s) {
  604. return s.split('\n').map(s => ` ${s}`).join('\n');
  605. }
  606. async function openInStackOverflow() {
  607. const comment = `// ${g.title}
  608. // from ${g.url}
  609. `;
  610. getSourcesFromEditor();
  611. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  612. const code = await fixJSForCodeSite(scripts.js);
  613. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  614. const mainHTML = scripts.html + html;
  615. const mainJS = comment + code;
  616. const mainCSS = htmlParts.css.sources[0].source;
  617. const asModule = /\bimport\b/.test(mainJS);
  618. // Three.js wants us to use modules but Stack Overflow doesn't support them
  619. const text = asModule
  620. ? `
  621. <!-- begin snippet: js hide: false console: true babel: false -->
  622. <!-- language: lang-js -->
  623. <!-- language: lang-css -->
  624. ${indent4(mainCSS)}
  625. <!-- language: lang-html -->
  626. ${indent4(mainHTML)}
  627. <script type="module">
  628. ${indent4(mainJS)}
  629. </script>
  630. <!-- end snippet -->
  631. `
  632. : `
  633. <!-- begin snippet: js hide: false console: true babel: false -->
  634. <!-- language: lang-js -->
  635. ${indent4(mainJS)}
  636. <!-- language: lang-css -->
  637. ${indent4(mainCSS)}
  638. <!-- language: lang-html -->
  639. ${indent4(mainHTML)}
  640. <!-- end snippet -->
  641. `;
  642. const dialogElem = document.querySelector('.copy-dialog');
  643. dialogElem.style.display = '';
  644. const copyAreaElem = dialogElem.querySelector('.copy-area');
  645. copyAreaElem.textContent = text;
  646. const linkElem = dialogElem.querySelector('a');
  647. const tags = lessonEditorSettings.tags.filter(f => !f.endsWith('.org')).join(' ');
  648. linkElem.href = `https://stackoverflow.com/questions/ask?&tags=javascript ${tags}`;
  649. }
  650. function htmlTemplate(s) {
  651. return `<!DOCTYPE html>
  652. <html>
  653. <head>
  654. <meta charset="utf-8">
  655. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
  656. <title>${s.title}</title>
  657. <style>
  658. ${s.css}
  659. </style>
  660. </head>
  661. <body>
  662. ${s.body}
  663. </body>
  664. ${s.script.startsWith('<')
  665. ? s.script
  666. : `
  667. <script type="module">
  668. ${s.script}
  669. </script>
  670. `}
  671. </html>`;
  672. }
  673. // ---vvv---
  674. // Copyright (c) 2013 Pieroxy <[email protected]>
  675. // This work is free. You can redistribute it and/or modify it
  676. // under the terms of the WTFPL, Version 2
  677. // For more information see LICENSE.txt or http://www.wtfpl.net/
  678. //
  679. // For more information, the home page:
  680. // http://pieroxy.net/blog/pages/lz-string/testing.html
  681. //
  682. // LZ-based compression algorithm, version 1.4.4
  683. //
  684. // Modified:
  685. // private property
  686. const keyStrBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  687. function compressToBase64(input) {
  688. if (input === null) {
  689. return '';
  690. }
  691. const res = _compress(input, 6, function(a) {
  692. return keyStrBase64.charAt(a);
  693. });
  694. switch (res.length % 4) { // To produce valid Base64
  695. default: // When could this happen ?
  696. case 0 : return res;
  697. case 1 : return res + '===';
  698. case 2 : return res + '==';
  699. case 3 : return res + '=';
  700. }
  701. }
  702. function _compress(uncompressed, bitsPerChar, getCharFromInt) {
  703. let i;
  704. let value;
  705. const context_dictionary = {};
  706. const context_dictionaryToCreate = {};
  707. let context_c = '';
  708. let context_wc = '';
  709. let context_w = '';
  710. let context_enlargeIn = 2; // Compensate for the first entry which should not count
  711. let context_dictSize = 3;
  712. let context_numBits = 2;
  713. const context_data = [];
  714. let context_data_val = 0;
  715. let context_data_position = 0;
  716. let ii;
  717. for (ii = 0; ii < uncompressed.length; ii += 1) {
  718. context_c = uncompressed.charAt(ii);
  719. if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
  720. context_dictionary[context_c] = context_dictSize++;
  721. context_dictionaryToCreate[context_c] = true;
  722. }
  723. context_wc = context_w + context_c;
  724. if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
  725. context_w = context_wc;
  726. } else {
  727. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  728. if (context_w.charCodeAt(0) < 256) {
  729. for (i = 0; i < context_numBits; i++) {
  730. context_data_val = (context_data_val << 1);
  731. if (context_data_position === bitsPerChar - 1) {
  732. context_data_position = 0;
  733. context_data.push(getCharFromInt(context_data_val));
  734. context_data_val = 0;
  735. } else {
  736. context_data_position++;
  737. }
  738. }
  739. value = context_w.charCodeAt(0);
  740. for (i = 0; i < 8; i++) {
  741. context_data_val = (context_data_val << 1) | (value & 1);
  742. if (context_data_position === bitsPerChar - 1) {
  743. context_data_position = 0;
  744. context_data.push(getCharFromInt(context_data_val));
  745. context_data_val = 0;
  746. } else {
  747. context_data_position++;
  748. }
  749. value = value >> 1;
  750. }
  751. } else {
  752. value = 1;
  753. for (i = 0; i < context_numBits; i++) {
  754. context_data_val = (context_data_val << 1) | value;
  755. if (context_data_position === bitsPerChar - 1) {
  756. context_data_position = 0;
  757. context_data.push(getCharFromInt(context_data_val));
  758. context_data_val = 0;
  759. } else {
  760. context_data_position++;
  761. }
  762. value = 0;
  763. }
  764. value = context_w.charCodeAt(0);
  765. for (i = 0; i < 16; i++) {
  766. context_data_val = (context_data_val << 1) | (value & 1);
  767. if (context_data_position === bitsPerChar - 1) {
  768. context_data_position = 0;
  769. context_data.push(getCharFromInt(context_data_val));
  770. context_data_val = 0;
  771. } else {
  772. context_data_position++;
  773. }
  774. value = value >> 1;
  775. }
  776. }
  777. context_enlargeIn--;
  778. if (context_enlargeIn === 0) {
  779. context_enlargeIn = Math.pow(2, context_numBits);
  780. context_numBits++;
  781. }
  782. delete context_dictionaryToCreate[context_w];
  783. } else {
  784. value = context_dictionary[context_w];
  785. for (i = 0; i < context_numBits; i++) {
  786. context_data_val = (context_data_val << 1) | (value & 1);
  787. if (context_data_position === bitsPerChar - 1) {
  788. context_data_position = 0;
  789. context_data.push(getCharFromInt(context_data_val));
  790. context_data_val = 0;
  791. } else {
  792. context_data_position++;
  793. }
  794. value = value >> 1;
  795. }
  796. }
  797. context_enlargeIn--;
  798. if (context_enlargeIn === 0) {
  799. context_enlargeIn = Math.pow(2, context_numBits);
  800. context_numBits++;
  801. }
  802. // Add wc to the dictionary.
  803. context_dictionary[context_wc] = context_dictSize++;
  804. context_w = String(context_c);
  805. }
  806. }
  807. // Output the code for w.
  808. if (context_w !== '') {
  809. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  810. if (context_w.charCodeAt(0) < 256) {
  811. for (i = 0; i < context_numBits; i++) {
  812. context_data_val = (context_data_val << 1);
  813. if (context_data_position === bitsPerChar - 1) {
  814. context_data_position = 0;
  815. context_data.push(getCharFromInt(context_data_val));
  816. context_data_val = 0;
  817. } else {
  818. context_data_position++;
  819. }
  820. }
  821. value = context_w.charCodeAt(0);
  822. for (i = 0; i < 8; i++) {
  823. context_data_val = (context_data_val << 1) | (value & 1);
  824. if (context_data_position === bitsPerChar - 1) {
  825. context_data_position = 0;
  826. context_data.push(getCharFromInt(context_data_val));
  827. context_data_val = 0;
  828. } else {
  829. context_data_position++;
  830. }
  831. value = value >> 1;
  832. }
  833. } else {
  834. value = 1;
  835. for (i = 0; i < context_numBits; i++) {
  836. context_data_val = (context_data_val << 1) | value;
  837. if (context_data_position === bitsPerChar - 1) {
  838. context_data_position = 0;
  839. context_data.push(getCharFromInt(context_data_val));
  840. context_data_val = 0;
  841. } else {
  842. context_data_position++;
  843. }
  844. value = 0;
  845. }
  846. value = context_w.charCodeAt(0);
  847. for (i = 0; i < 16; i++) {
  848. context_data_val = (context_data_val << 1) | (value & 1);
  849. if (context_data_position === bitsPerChar - 1) {
  850. context_data_position = 0;
  851. context_data.push(getCharFromInt(context_data_val));
  852. context_data_val = 0;
  853. } else {
  854. context_data_position++;
  855. }
  856. value = value >> 1;
  857. }
  858. }
  859. context_enlargeIn--;
  860. if (context_enlargeIn === 0) {
  861. context_enlargeIn = Math.pow(2, context_numBits);
  862. context_numBits++;
  863. }
  864. delete context_dictionaryToCreate[context_w];
  865. } else {
  866. value = context_dictionary[context_w];
  867. for (i = 0; i < context_numBits; i++) {
  868. context_data_val = (context_data_val << 1) | (value & 1);
  869. if (context_data_position === bitsPerChar - 1) {
  870. context_data_position = 0;
  871. context_data.push(getCharFromInt(context_data_val));
  872. context_data_val = 0;
  873. } else {
  874. context_data_position++;
  875. }
  876. value = value >> 1;
  877. }
  878. }
  879. context_enlargeIn--;
  880. if (context_enlargeIn === 0) {
  881. context_numBits++;
  882. }
  883. }
  884. // Mark the end of the stream
  885. value = 2;
  886. for (i = 0; i < context_numBits; i++) {
  887. context_data_val = (context_data_val << 1) | (value & 1);
  888. if (context_data_position === bitsPerChar - 1) {
  889. context_data_position = 0;
  890. context_data.push(getCharFromInt(context_data_val));
  891. context_data_val = 0;
  892. } else {
  893. context_data_position++;
  894. }
  895. value = value >> 1;
  896. }
  897. // Flush the last char
  898. for (;;) {
  899. context_data_val = (context_data_val << 1);
  900. if (context_data_position === bitsPerChar - 1) {
  901. context_data.push(getCharFromInt(context_data_val));
  902. break;
  903. } else {
  904. context_data_position++;
  905. }
  906. }
  907. return context_data.join('');
  908. }
  909. function compress(input) {
  910. return compressToBase64(input)
  911. .replace(/\+/g, '-') // Convert '+' to '-'
  912. .replace(/\//g, '_') // Convert '/' to '_'
  913. .replace(/=+$/, ''); // Remove ending '='
  914. }
  915. function getParameters(parameters) {
  916. return compress(JSON.stringify(parameters));
  917. }
  918. // -- ^^^ ---
  919. async function openInCodeSandbox() {
  920. const comment = `// ${g.title}
  921. // from ${g.url}
  922. `;
  923. getSourcesFromEditor();
  924. const scripts = getScripts(g.rootScriptInfo);
  925. const mainScript = scripts.pop();
  926. const code = await fixJSForCodeSite(mainScript.text);
  927. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  928. const names = scripts.map(s => s.name);
  929. const files = scripts.reduce((files, {name, text: content}) => {
  930. files[name] = {content};
  931. return files;
  932. }, {
  933. 'index.html': {
  934. content: htmlTemplate({
  935. body: html,
  936. css: htmlParts.css.sources[0].source,
  937. title: g.title,
  938. script: comment + code,
  939. }),
  940. },
  941. 'sandbox.config.json': {
  942. content: '{\n "template": "static"\n}\n',
  943. },
  944. 'package.json': {
  945. content: JSON.stringify({
  946. 'name': 'static',
  947. 'version': '1.0.0',
  948. 'description': 'This is a static template with no bundling',
  949. 'main': 'index.html',
  950. 'scripts': {
  951. 'start': 'serve',
  952. 'build': 'echo This is a static template, there is no bundler or bundling involved!',
  953. },
  954. 'license': 'MIT',
  955. 'devDependencies': {
  956. 'serve': '^11.2.0',
  957. },
  958. }, null, 2),
  959. },
  960. });
  961. for (const file of Object.values(files)) {
  962. for (const name of names) {
  963. file.content = file.content.split(name).join(`./${name}`);
  964. }
  965. }
  966. const parameters = getParameters({files});
  967. const elem = document.createElement('div');
  968. elem.innerHTML = `
  969. <form action="https://codesandbox.io/api/v1/sandboxes/define" method="POST" target="_blank" class="hidden">
  970. <input type="hidden" name="parameters" />
  971. <input type="submit" />
  972. </form>
  973. `;
  974. elem.querySelector('input[name=parameters]').value = parameters;
  975. window.frameElement.ownerDocument.body.appendChild(elem);
  976. elem.querySelector('form').submit();
  977. window.frameElement.ownerDocument.body.removeChild(elem);
  978. }
  979. /*
  980. async function openInStackBlitz() {
  981. const comment = `// ${g.title}
  982. // from ${g.url}
  983. `;
  984. getSourcesFromEditor();
  985. const scripts = getScripts(g.rootScriptInfo);
  986. const code = await fixJSForCodeSite(scripts.js);
  987. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  988. const mainScript = scripts.pop();
  989. const names = scripts.map(s => s.name);
  990. const files = scripts.reduce((files, {name, text: content}) => {
  991. files[name] = {content};
  992. return files;
  993. }, {
  994. 'index.html': {
  995. content: htmlTemplate({
  996. body: html,
  997. css: htmlParts.css.sources[0].source,
  998. title: g.title,
  999. script: '<script src="index.js" type="module"></script>',
  1000. }),
  1001. },
  1002. 'index.js': {
  1003. content: comment + code,
  1004. },
  1005. // "tsconfig.json": {
  1006. // content: JSON.stringify({
  1007. // "compilerOptions": {
  1008. // "target": "esnext"
  1009. // }
  1010. // }, null, 2),
  1011. // },
  1012. 'package.json': {
  1013. content: JSON.stringify({
  1014. 'name': 'js',
  1015. 'version': '0.0.0',
  1016. 'private': true,
  1017. 'dependencies': {}
  1018. }, null, 2),
  1019. }
  1020. });
  1021. const elem = document.createElement('div');
  1022. elem.innerHTML = `
  1023. <form action="https://stackblitz.com/run" method="POST" target="_blank" class="hidden">
  1024. <input type="hidden" name="project[description]" value="${g.title}">
  1025. <input type="hidden" name="project[dependencies]" value="{}">
  1026. <input type="hidden" name="project[template]" value="javascript">
  1027. <input type="hidden" name="project[settings]" value="{}">
  1028. <input type="submit" />
  1029. </form>
  1030. `;
  1031. const form = elem.querySelector('form');
  1032. for (const [name, file] of Object.entries(files)) {
  1033. for (const name of names) {
  1034. file.content = file.content.split(name).join(`./${name}`);
  1035. }
  1036. const input = document.createElement('input');
  1037. input.type = 'hidden';
  1038. input.name = `project[files][${name}]`;
  1039. input.value = file.content;
  1040. form.appendChild(input);
  1041. }
  1042. window.frameElement.ownerDocument.body.appendChild(elem);
  1043. form.submit();
  1044. window.frameElement.ownerDocument.body.removeChild(elem);
  1045. }
  1046. */
  1047. document.querySelectorAll('.dialog').forEach(dialogElem => {
  1048. dialogElem.addEventListener('click', function(e) {
  1049. if (e.target === this) {
  1050. this.style.display = 'none';
  1051. }
  1052. });
  1053. dialogElem.addEventListener('keydown', function(e) {
  1054. console.log(e.keyCode);
  1055. if (e.keyCode === 27) {
  1056. this.style.display = 'none';
  1057. }
  1058. });
  1059. });
  1060. const exportDialogElem = document.querySelector('.export');
  1061. function openExport() {
  1062. exportDialogElem.style.display = '';
  1063. exportDialogElem.firstElementChild.focus();
  1064. }
  1065. function closeExport(fn) {
  1066. return () => {
  1067. exportDialogElem.style.display = 'none';
  1068. fn();
  1069. };
  1070. }
  1071. document.querySelector('.button-export').addEventListener('click', openExport);
  1072. function selectFile(info, ndx, fileDivs) {
  1073. if (info.editors.length <= 1) {
  1074. return;
  1075. }
  1076. info.editors.forEach((editorInfo, i) => {
  1077. const selected = i === ndx;
  1078. editorInfo.div.style.display = selected ? '' : 'none';
  1079. editorInfo.editor.layout();
  1080. addRemoveClass(fileDivs.children[i], 'fileSelected', selected);
  1081. });
  1082. }
  1083. function showEditorSubPane(type, ndx) {
  1084. const info = htmlParts[type];
  1085. selectFile(info, ndx, info.files);
  1086. }
  1087. function setupEditor() {
  1088. forEachHTMLPart(function(info, ndx, name) {
  1089. info.pane = document.querySelector('.panes>.' + name);
  1090. info.code = info.pane.querySelector('.code');
  1091. info.files = info.pane.querySelector('.files');
  1092. info.editors = info.sources.map((sourceInfo, ndx) => {
  1093. if (info.sources.length > 1) {
  1094. const div = document.createElement('div');
  1095. div.textContent = basename(sourceInfo.name);
  1096. info.files.appendChild(div);
  1097. div.addEventListener('click', () => {
  1098. selectFile(info, ndx, info.files);
  1099. });
  1100. }
  1101. const div = document.createElement('div');
  1102. info.code.appendChild(div);
  1103. const editor = runEditor(div, sourceInfo.source, info.language);
  1104. sourceInfo.editor = editor;
  1105. return {
  1106. div,
  1107. editor,
  1108. };
  1109. });
  1110. info.button = document.querySelector('.button-' + name);
  1111. info.button.addEventListener('click', function() {
  1112. toggleSourcePane(info.button);
  1113. runIfNeeded();
  1114. });
  1115. });
  1116. g.fullscreen = document.querySelector('.button-fullscreen');
  1117. g.fullscreen.addEventListener('click', toggleFullscreen);
  1118. g.run = document.querySelector('.button-run');
  1119. g.run.addEventListener('click', run);
  1120. g.iframe = document.querySelector('.result>iframe');
  1121. g.other = document.querySelector('.panes .other');
  1122. document.querySelector('.button-codepen').addEventListener('click', closeExport(openInCodepen));
  1123. document.querySelector('.button-jsfiddle').addEventListener('click', closeExport(openInJSFiddle));
  1124. document.querySelector('.button-jsgist').addEventListener('click', closeExport(openInJSGist));
  1125. document.querySelector('.button-stackoverflow').addEventListener('click', closeExport(openInStackOverflow));
  1126. document.querySelector('.button-codesandbox').addEventListener('click', closeExport(openInCodeSandbox));
  1127. //document.querySelector('.button-stackblitz').addEventListener('click', openInStackBlitz);
  1128. g.result = document.querySelector('.panes .result');
  1129. g.resultButton = document.querySelector('.button-result');
  1130. g.resultButton.addEventListener('click', function() {
  1131. toggleResultPane();
  1132. runIfNeeded();
  1133. });
  1134. g.result.style.display = 'none';
  1135. toggleResultPane();
  1136. if (window.innerWidth >= 1000) {
  1137. toggleSourcePane(htmlParts.js.button);
  1138. }
  1139. window.addEventListener('resize', resize);
  1140. showEditorSubPane('js', 0);
  1141. showOtherIfAllPanesOff();
  1142. document.querySelector('.other .loading').style.display = 'none';
  1143. resize();
  1144. run();
  1145. }
  1146. function toggleFullscreen() {
  1147. try {
  1148. toggleIFrameFullscreen(window);
  1149. resize();
  1150. runIfNeeded();
  1151. } catch (e) {
  1152. console.error(e); // eslint-disable-line
  1153. }
  1154. }
  1155. function runIfNeeded() {
  1156. if (runOnResize) {
  1157. run();
  1158. }
  1159. }
  1160. function run() {
  1161. g.setPosition = false;
  1162. const url = getSourceBlobFromEditor();
  1163. g.iframe.src = url;
  1164. }
  1165. function addClass(elem, className) {
  1166. const parts = elem.className.split(' ');
  1167. if (parts.indexOf(className) < 0) {
  1168. elem.className = elem.className + ' ' + className;
  1169. }
  1170. }
  1171. function removeClass(elem, className) {
  1172. const parts = elem.className.split(' ');
  1173. const numParts = parts.length;
  1174. for (;;) {
  1175. const ndx = parts.indexOf(className);
  1176. if (ndx < 0) {
  1177. break;
  1178. }
  1179. parts.splice(ndx, 1);
  1180. }
  1181. if (parts.length !== numParts) {
  1182. elem.className = parts.join(' ');
  1183. return true;
  1184. }
  1185. return false;
  1186. }
  1187. function toggleClass(elem, className) {
  1188. if (removeClass(elem, className)) {
  1189. return false;
  1190. } else {
  1191. addClass(elem, className);
  1192. return true;
  1193. }
  1194. }
  1195. function toggleIFrameFullscreen(childWindow) {
  1196. const frame = childWindow.frameElement;
  1197. if (frame) {
  1198. const isFullScreen = toggleClass(frame, 'fullscreen');
  1199. frame.ownerDocument.body.style.overflow = isFullScreen ? 'hidden' : '';
  1200. }
  1201. }
  1202. function addRemoveClass(elem, className, add) {
  1203. if (add) {
  1204. addClass(elem, className);
  1205. } else {
  1206. removeClass(elem, className);
  1207. }
  1208. }
  1209. function toggleSourcePane(pressedButton) {
  1210. forEachHTMLPart(function(info) {
  1211. const pressed = pressedButton === info.button;
  1212. if (pressed && !info.showing) {
  1213. addClass(info.button, 'show');
  1214. info.pane.style.display = 'flex';
  1215. info.showing = true;
  1216. } else {
  1217. removeClass(info.button, 'show');
  1218. info.pane.style.display = 'none';
  1219. info.showing = false;
  1220. }
  1221. });
  1222. showOtherIfAllPanesOff();
  1223. resize();
  1224. }
  1225. function showingResultPane() {
  1226. return g.result.style.display !== 'none';
  1227. }
  1228. function toggleResultPane() {
  1229. const showing = showingResultPane();
  1230. g.result.style.display = showing ? 'none' : 'block';
  1231. addRemoveClass(g.resultButton, 'show', !showing);
  1232. showOtherIfAllPanesOff();
  1233. resize();
  1234. }
  1235. function showOtherIfAllPanesOff() {
  1236. let paneOn = showingResultPane();
  1237. forEachHTMLPart(function(info) {
  1238. paneOn = paneOn || info.showing;
  1239. });
  1240. g.other.style.display = paneOn ? 'none' : 'block';
  1241. }
  1242. // seems like we should probably store a map
  1243. function getEditorNdxByBlobUrl(type, url) {
  1244. return htmlParts[type].sources.findIndex(source => source.scriptInfo.blobUrl === url);
  1245. }
  1246. function getActualLineNumberAndMoveTo(url, lineNo, colNo) {
  1247. let origUrl = url;
  1248. let actualLineNo = lineNo;
  1249. const scriptInfo = Object.values(g.scriptInfos).find(scriptInfo => scriptInfo.blobUrl === url);
  1250. if (scriptInfo) {
  1251. actualLineNo = lineNo - scriptInfo.numLinesBeforeScript;
  1252. origUrl = basename(scriptInfo.fqURL);
  1253. if (!g.setPosition) {
  1254. // Only set the first position
  1255. g.setPosition = true;
  1256. const editorNdx = getEditorNdxByBlobUrl('js', url);
  1257. if (editorNdx >= 0) {
  1258. showEditorSubPane('js', editorNdx);
  1259. const editor = htmlParts.js.editors[editorNdx].editor;
  1260. editor.setPosition({
  1261. lineNumber: actualLineNo,
  1262. column: colNo,
  1263. });
  1264. editor.revealLineInCenterIfOutsideViewport(actualLineNo);
  1265. editor.focus();
  1266. }
  1267. }
  1268. }
  1269. return {origUrl, actualLineNo};
  1270. }
  1271. window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo;
  1272. function runEditor(parent, source, language) {
  1273. return monaco.editor.create(parent, {
  1274. value: source,
  1275. language: language,
  1276. //lineNumbers: false,
  1277. theme: 'vs-dark',
  1278. disableTranslate3d: true,
  1279. // model: null,
  1280. scrollBeyondLastLine: false,
  1281. minimap: { enabled: false },
  1282. });
  1283. }
  1284. async function runAsBlob() {
  1285. const query = getQuery();
  1286. g.url = getFQUrl(query.url);
  1287. g.query = getSearch(g.url);
  1288. let html;
  1289. try {
  1290. html = await getHTML(query.url);
  1291. } catch (err) {
  1292. console.log(err); // eslint-disable-line
  1293. return;
  1294. }
  1295. await parseHTML(query.url, html);
  1296. window.location.href = getSourceBlobFromOrig();
  1297. }
  1298. function applySubstitutions() {
  1299. [...document.querySelectorAll('[data-subst]')].forEach((elem) => {
  1300. elem.dataset.subst.split('&').forEach((pair) => {
  1301. const [attr, key] = pair.split('|');
  1302. elem[attr] = lessonEditorSettings[key];
  1303. });
  1304. });
  1305. }
  1306. function start() {
  1307. const parentQuery = getQuery(window.parent.location.search);
  1308. const isSmallish = window.navigator.userAgent.match(/Android|iPhone|iPod|Windows Phone/i);
  1309. const isEdge = window.navigator.userAgent.match(/Edge/i);
  1310. if (isEdge || isSmallish || parentQuery.editor === 'false') {
  1311. runAsBlob();
  1312. // var url = query.url;
  1313. // window.location.href = url;
  1314. } else {
  1315. applySubstitutions();
  1316. require.config({ paths: { 'vs': '/manual/3rdparty/monaco-editor/min/vs' }});
  1317. require(['vs/editor/editor.main'], main);
  1318. }
  1319. }
  1320. start();
  1321. }());