editor.js 42 KB

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