editor.js 45 KB

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