editor.js 42 KB

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