editor.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971
  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 <[email protected]>
  675. // This work is free. You can redistribute it and/or modify it
  676. // under the terms of the WTFPL, Version 2
  677. // For more information see LICENSE.txt or http://www.wtfpl.net/
  678. //
  679. // For more information, the home page:
  680. // http://pieroxy.net/blog/pages/lz-string/testing.html
  681. //
  682. // LZ-based compression algorithm, version 1.4.4
  683. //
  684. // Modified:
  685. // private property
  686. const keyStrBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
  687. function compressToBase64( input ) {
  688. if ( input === null ) {
  689. return '';
  690. }
  691. const res = _compress( input, 6, function ( a ) {
  692. return keyStrBase64.charAt( a );
  693. } );
  694. switch ( res.length % 4 ) { // To produce valid Base64
  695. default: // When could this happen ?
  696. case 0 : return res;
  697. case 1 : return res + '===';
  698. case 2 : return res + '==';
  699. case 3 : return res + '=';
  700. }
  701. }
  702. function _compress( uncompressed, bitsPerChar, getCharFromInt ) {
  703. let i;
  704. let value;
  705. const context_dictionary = {};
  706. const context_dictionaryToCreate = {};
  707. let context_c = '';
  708. let context_wc = '';
  709. let context_w = '';
  710. let context_enlargeIn = 2; // Compensate for the first entry which should not count
  711. let context_dictSize = 3;
  712. let context_numBits = 2;
  713. const context_data = [];
  714. let context_data_val = 0;
  715. let context_data_position = 0;
  716. let ii;
  717. for ( ii = 0; ii < uncompressed.length; ii += 1 ) {
  718. context_c = uncompressed.charAt( ii );
  719. if ( ! Object.prototype.hasOwnProperty.call( context_dictionary, context_c ) ) {
  720. context_dictionary[ context_c ] = context_dictSize ++;
  721. context_dictionaryToCreate[ context_c ] = true;
  722. }
  723. context_wc = context_w + context_c;
  724. if ( Object.prototype.hasOwnProperty.call( context_dictionary, context_wc ) ) {
  725. context_w = context_wc;
  726. } else {
  727. if ( Object.prototype.hasOwnProperty.call( context_dictionaryToCreate, context_w ) ) {
  728. if ( context_w.charCodeAt( 0 ) < 256 ) {
  729. for ( i = 0; i < context_numBits; i ++ ) {
  730. context_data_val = ( context_data_val << 1 );
  731. if ( context_data_position === bitsPerChar - 1 ) {
  732. context_data_position = 0;
  733. context_data.push( getCharFromInt( context_data_val ) );
  734. context_data_val = 0;
  735. } else {
  736. context_data_position ++;
  737. }
  738. }
  739. value = context_w.charCodeAt( 0 );
  740. for ( i = 0; i < 8; i ++ ) {
  741. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  742. if ( context_data_position === bitsPerChar - 1 ) {
  743. context_data_position = 0;
  744. context_data.push( getCharFromInt( context_data_val ) );
  745. context_data_val = 0;
  746. } else {
  747. context_data_position ++;
  748. }
  749. value = value >> 1;
  750. }
  751. } else {
  752. value = 1;
  753. for ( i = 0; i < context_numBits; i ++ ) {
  754. context_data_val = ( context_data_val << 1 ) | value;
  755. if ( context_data_position === bitsPerChar - 1 ) {
  756. context_data_position = 0;
  757. context_data.push( getCharFromInt( context_data_val ) );
  758. context_data_val = 0;
  759. } else {
  760. context_data_position ++;
  761. }
  762. value = 0;
  763. }
  764. value = context_w.charCodeAt( 0 );
  765. for ( i = 0; i < 16; i ++ ) {
  766. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  767. if ( context_data_position === bitsPerChar - 1 ) {
  768. context_data_position = 0;
  769. context_data.push( getCharFromInt( context_data_val ) );
  770. context_data_val = 0;
  771. } else {
  772. context_data_position ++;
  773. }
  774. value = value >> 1;
  775. }
  776. }
  777. context_enlargeIn --;
  778. if ( context_enlargeIn === 0 ) {
  779. context_enlargeIn = Math.pow( 2, context_numBits );
  780. context_numBits ++;
  781. }
  782. delete context_dictionaryToCreate[ context_w ];
  783. } else {
  784. value = context_dictionary[ context_w ];
  785. for ( i = 0; i < context_numBits; i ++ ) {
  786. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  787. if ( context_data_position === bitsPerChar - 1 ) {
  788. context_data_position = 0;
  789. context_data.push( getCharFromInt( context_data_val ) );
  790. context_data_val = 0;
  791. } else {
  792. context_data_position ++;
  793. }
  794. value = value >> 1;
  795. }
  796. }
  797. context_enlargeIn --;
  798. if ( context_enlargeIn === 0 ) {
  799. context_enlargeIn = Math.pow( 2, context_numBits );
  800. context_numBits ++;
  801. }
  802. // Add wc to the dictionary.
  803. context_dictionary[ context_wc ] = context_dictSize ++;
  804. context_w = String( context_c );
  805. }
  806. }
  807. // Output the code for w.
  808. if ( context_w !== '' ) {
  809. if ( Object.prototype.hasOwnProperty.call( context_dictionaryToCreate, context_w ) ) {
  810. if ( context_w.charCodeAt( 0 ) < 256 ) {
  811. for ( i = 0; i < context_numBits; i ++ ) {
  812. context_data_val = ( context_data_val << 1 );
  813. if ( context_data_position === bitsPerChar - 1 ) {
  814. context_data_position = 0;
  815. context_data.push( getCharFromInt( context_data_val ) );
  816. context_data_val = 0;
  817. } else {
  818. context_data_position ++;
  819. }
  820. }
  821. value = context_w.charCodeAt( 0 );
  822. for ( i = 0; i < 8; i ++ ) {
  823. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  824. if ( context_data_position === bitsPerChar - 1 ) {
  825. context_data_position = 0;
  826. context_data.push( getCharFromInt( context_data_val ) );
  827. context_data_val = 0;
  828. } else {
  829. context_data_position ++;
  830. }
  831. value = value >> 1;
  832. }
  833. } else {
  834. value = 1;
  835. for ( i = 0; i < context_numBits; i ++ ) {
  836. context_data_val = ( context_data_val << 1 ) | value;
  837. if ( context_data_position === bitsPerChar - 1 ) {
  838. context_data_position = 0;
  839. context_data.push( getCharFromInt( context_data_val ) );
  840. context_data_val = 0;
  841. } else {
  842. context_data_position ++;
  843. }
  844. value = 0;
  845. }
  846. value = context_w.charCodeAt( 0 );
  847. for ( i = 0; i < 16; i ++ ) {
  848. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  849. if ( context_data_position === bitsPerChar - 1 ) {
  850. context_data_position = 0;
  851. context_data.push( getCharFromInt( context_data_val ) );
  852. context_data_val = 0;
  853. } else {
  854. context_data_position ++;
  855. }
  856. value = value >> 1;
  857. }
  858. }
  859. context_enlargeIn --;
  860. if ( context_enlargeIn === 0 ) {
  861. context_enlargeIn = Math.pow( 2, context_numBits );
  862. context_numBits ++;
  863. }
  864. delete context_dictionaryToCreate[ context_w ];
  865. } else {
  866. value = context_dictionary[ context_w ];
  867. for ( i = 0; i < context_numBits; i ++ ) {
  868. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  869. if ( context_data_position === bitsPerChar - 1 ) {
  870. context_data_position = 0;
  871. context_data.push( getCharFromInt( context_data_val ) );
  872. context_data_val = 0;
  873. } else {
  874. context_data_position ++;
  875. }
  876. value = value >> 1;
  877. }
  878. }
  879. context_enlargeIn --;
  880. if ( context_enlargeIn === 0 ) {
  881. context_numBits ++;
  882. }
  883. }
  884. // Mark the end of the stream
  885. value = 2;
  886. for ( i = 0; i < context_numBits; i ++ ) {
  887. context_data_val = ( context_data_val << 1 ) | ( value & 1 );
  888. if ( context_data_position === bitsPerChar - 1 ) {
  889. context_data_position = 0;
  890. context_data.push( getCharFromInt( context_data_val ) );
  891. context_data_val = 0;
  892. } else {
  893. context_data_position ++;
  894. }
  895. value = value >> 1;
  896. }
  897. // Flush the last char
  898. for ( ;; ) {
  899. context_data_val = ( context_data_val << 1 );
  900. if ( context_data_position === bitsPerChar - 1 ) {
  901. context_data.push( getCharFromInt( context_data_val ) );
  902. break;
  903. } else {
  904. context_data_position ++;
  905. }
  906. }
  907. return context_data.join( '' );
  908. }
  909. function compress( input ) {
  910. return compressToBase64( input )
  911. .replace( /\+/g, '-' ) // Convert '+' to '-'
  912. .replace( /\//g, '_' ) // Convert '/' to '_'
  913. .replace( /=+$/, '' ); // Remove ending '='
  914. }
  915. function getParameters( parameters ) {
  916. return compress( JSON.stringify( parameters ) );
  917. }
  918. // -- ^^^ ---
  919. async function openInCodeSandbox() {
  920. const comment = `// ${g.title}
  921. // from ${g.url}
  922. `;
  923. getSourcesFromEditor();
  924. const scripts = getScripts( g.rootScriptInfo );
  925. const mainScript = scripts.pop();
  926. const code = await fixJSForCodeSite( mainScript.text );
  927. const html = await fixHTMLForCodeSite( htmlParts.html.sources[ 0 ].source );
  928. const names = scripts.map( s => s.name );
  929. const files = scripts.reduce( ( files, { name, text: content } ) => {
  930. files[ name ] = { content };
  931. return files;
  932. }, {
  933. 'index.html': {
  934. content: htmlTemplate( {
  935. body: html,
  936. css: htmlParts.css.sources[ 0 ].source,
  937. title: g.title,
  938. script: comment + code,
  939. } ),
  940. },
  941. 'sandbox.config.json': {
  942. content: '{\n "template": "static"\n}\n',
  943. },
  944. 'package.json': {
  945. content: JSON.stringify( {
  946. 'name': 'static',
  947. 'version': '1.0.0',
  948. 'description': 'This is a static template with no bundling',
  949. 'main': 'index.html',
  950. 'scripts': {
  951. 'start': 'serve',
  952. 'build': 'echo This is a static template, there is no bundler or bundling involved!',
  953. },
  954. 'license': 'MIT',
  955. 'devDependencies': {
  956. 'serve': '^11.2.0',
  957. },
  958. }, null, 2 ),
  959. },
  960. } );
  961. for ( const file of Object.values( files ) ) {
  962. for ( const name of names ) {
  963. file.content = file.content.split( name ).join( `./${name}` );
  964. }
  965. }
  966. const parameters = getParameters( { files } );
  967. const elem = document.createElement( 'div' );
  968. elem.innerHTML = `
  969. <form action="https://codesandbox.io/api/v1/sandboxes/define" method="POST" target="_blank" class="hidden">
  970. <input type="hidden" name="parameters" />
  971. <input type="submit" />
  972. </form>
  973. `;
  974. elem.querySelector( 'input[name=parameters]' ).value = parameters;
  975. window.frameElement.ownerDocument.body.appendChild( elem );
  976. elem.querySelector( 'form' ).submit();
  977. window.frameElement.ownerDocument.body.removeChild( elem );
  978. }
  979. /*
  980. async function openInStackBlitz() {
  981. const comment = `// ${g.title}
  982. // from ${g.url}
  983. `;
  984. getSourcesFromEditor();
  985. const scripts = getScripts(g.rootScriptInfo);
  986. const code = await fixJSForCodeSite(scripts.js);
  987. const html = await fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  988. const mainScript = scripts.pop();
  989. const names = scripts.map(s => s.name);
  990. const files = scripts.reduce((files, {name, text: content}) => {
  991. files[name] = {content};
  992. return files;
  993. }, {
  994. 'index.html': {
  995. content: htmlTemplate({
  996. body: html,
  997. css: htmlParts.css.sources[0].source,
  998. title: g.title,
  999. script: '<script src="index.js" type="module"></script>',
  1000. }),
  1001. },
  1002. 'index.js': {
  1003. content: comment + code,
  1004. },
  1005. // "tsconfig.json": {
  1006. // content: JSON.stringify({
  1007. // "compilerOptions": {
  1008. // "target": "esnext"
  1009. // }
  1010. // }, null, 2),
  1011. // },
  1012. 'package.json': {
  1013. content: JSON.stringify({
  1014. 'name': 'js',
  1015. 'version': '0.0.0',
  1016. 'private': true,
  1017. 'dependencies': {}
  1018. }, null, 2),
  1019. }
  1020. });
  1021. const elem = document.createElement('div');
  1022. elem.innerHTML = `
  1023. <form action="https://stackblitz.com/run" method="POST" target="_blank" class="hidden">
  1024. <input type="hidden" name="project[description]" value="${g.title}">
  1025. <input type="hidden" name="project[dependencies]" value="{}">
  1026. <input type="hidden" name="project[template]" value="javascript">
  1027. <input type="hidden" name="project[settings]" value="{}">
  1028. <input type="submit" />
  1029. </form>
  1030. `;
  1031. const form = elem.querySelector('form');
  1032. for (const [name, file] of Object.entries(files)) {
  1033. for (const name of names) {
  1034. file.content = file.content.split(name).join(`./${name}`);
  1035. }
  1036. const input = document.createElement('input');
  1037. input.type = 'hidden';
  1038. input.name = `project[files][${name}]`;
  1039. input.value = file.content;
  1040. form.appendChild(input);
  1041. }
  1042. window.frameElement.ownerDocument.body.appendChild(elem);
  1043. form.submit();
  1044. window.frameElement.ownerDocument.body.removeChild(elem);
  1045. }
  1046. */
  1047. document.querySelectorAll( '.dialog' ).forEach( dialogElem => {
  1048. dialogElem.addEventListener( 'click', function ( e ) {
  1049. if ( e.target === this ) {
  1050. this.style.display = 'none';
  1051. }
  1052. } );
  1053. dialogElem.addEventListener( 'keydown', function ( e ) {
  1054. console.log( e.keyCode );
  1055. if ( e.keyCode === 27 ) {
  1056. this.style.display = 'none';
  1057. }
  1058. } );
  1059. } );
  1060. const exportDialogElem = document.querySelector( '.export' );
  1061. function openExport() {
  1062. exportDialogElem.style.display = '';
  1063. exportDialogElem.firstElementChild.focus();
  1064. }
  1065. function closeExport( fn ) {
  1066. return () => {
  1067. exportDialogElem.style.display = 'none';
  1068. fn();
  1069. };
  1070. }
  1071. document.querySelector( '.button-export' ).addEventListener( 'click', openExport );
  1072. function selectFile( info, ndx, fileDivs ) {
  1073. if ( info.editors.length <= 1 ) {
  1074. return;
  1075. }
  1076. info.editors.forEach( ( editorInfo, i ) => {
  1077. const selected = i === ndx;
  1078. editorInfo.div.style.display = selected ? '' : 'none';
  1079. editorInfo.editor.layout();
  1080. addRemoveClass( fileDivs.children[ i ], 'fileSelected', selected );
  1081. } );
  1082. }
  1083. function showEditorSubPane( type, ndx ) {
  1084. const info = htmlParts[ type ];
  1085. selectFile( info, ndx, info.files );
  1086. }
  1087. function setupEditor() {
  1088. forEachHTMLPart( function ( info, ndx, name ) {
  1089. info.pane = document.querySelector( '.panes>.' + name );
  1090. info.code = info.pane.querySelector( '.code' );
  1091. info.files = info.pane.querySelector( '.files' );
  1092. info.editors = info.sources.map( ( sourceInfo, ndx ) => {
  1093. if ( info.sources.length > 1 ) {
  1094. const div = document.createElement( 'div' );
  1095. div.textContent = basename( sourceInfo.name );
  1096. info.files.appendChild( div );
  1097. div.addEventListener( 'click', () => {
  1098. selectFile( info, ndx, info.files );
  1099. } );
  1100. }
  1101. const div = document.createElement( 'div' );
  1102. info.code.appendChild( div );
  1103. const editor = runEditor( div, sourceInfo.source, info.language );
  1104. sourceInfo.editor = editor;
  1105. return {
  1106. div,
  1107. editor,
  1108. };
  1109. } );
  1110. info.button = document.querySelector( '.button-' + name );
  1111. info.button.addEventListener( 'click', function () {
  1112. toggleSourcePane( info.button );
  1113. runIfNeeded();
  1114. } );
  1115. } );
  1116. g.fullscreen = document.querySelector( '.button-fullscreen' );
  1117. g.fullscreen.addEventListener( 'click', toggleFullscreen );
  1118. g.run = document.querySelector( '.button-run' );
  1119. g.run.addEventListener( 'click', run );
  1120. g.iframe = document.querySelector( '.result>iframe' );
  1121. g.other = document.querySelector( '.panes .other' );
  1122. document.querySelector( '.button-codepen' ).addEventListener( 'click', closeExport( openInCodepen ) );
  1123. document.querySelector( '.button-jsfiddle' ).addEventListener( 'click', closeExport( openInJSFiddle ) );
  1124. document.querySelector( '.button-jsgist' ).addEventListener( 'click', closeExport( openInJSGist ) );
  1125. document.querySelector( '.button-stackoverflow' ).addEventListener( 'click', closeExport( openInStackOverflow ) );
  1126. document.querySelector( '.button-codesandbox' ).addEventListener( 'click', closeExport( openInCodeSandbox ) );
  1127. //document.querySelector('.button-stackblitz').addEventListener('click', openInStackBlitz);
  1128. g.result = document.querySelector( '.panes .result' );
  1129. g.resultButton = document.querySelector( '.button-result' );
  1130. g.resultButton.addEventListener( 'click', function () {
  1131. toggleResultPane();
  1132. runIfNeeded();
  1133. } );
  1134. g.result.style.display = 'none';
  1135. toggleResultPane();
  1136. if ( window.innerWidth >= 1000 ) {
  1137. toggleSourcePane( htmlParts.js.button );
  1138. }
  1139. window.addEventListener( 'resize', resize );
  1140. showEditorSubPane( 'js', 0 );
  1141. showOtherIfAllPanesOff();
  1142. document.querySelector( '.other .loading' ).style.display = 'none';
  1143. resize();
  1144. run();
  1145. }
  1146. function toggleFullscreen() {
  1147. try {
  1148. toggleIFrameFullscreen( window );
  1149. resize();
  1150. runIfNeeded();
  1151. } catch ( e ) {
  1152. console.error(e); // eslint-disable-line
  1153. }
  1154. }
  1155. function runIfNeeded() {
  1156. if ( runOnResize ) {
  1157. run();
  1158. }
  1159. }
  1160. function run() {
  1161. g.setPosition = false;
  1162. const url = getSourceBlobFromEditor();
  1163. g.iframe.src = url;
  1164. }
  1165. function addClass( elem, className ) {
  1166. const parts = elem.className.split( ' ' );
  1167. if ( parts.indexOf( className ) < 0 ) {
  1168. elem.className = elem.className + ' ' + className;
  1169. }
  1170. }
  1171. function removeClass( elem, className ) {
  1172. const parts = elem.className.split( ' ' );
  1173. const numParts = parts.length;
  1174. for ( ;; ) {
  1175. const ndx = parts.indexOf( className );
  1176. if ( ndx < 0 ) {
  1177. break;
  1178. }
  1179. parts.splice( ndx, 1 );
  1180. }
  1181. if ( parts.length !== numParts ) {
  1182. elem.className = parts.join( ' ' );
  1183. return true;
  1184. }
  1185. return false;
  1186. }
  1187. function toggleClass( elem, className ) {
  1188. if ( removeClass( elem, className ) ) {
  1189. return false;
  1190. } else {
  1191. addClass( elem, className );
  1192. return true;
  1193. }
  1194. }
  1195. function toggleIFrameFullscreen( childWindow ) {
  1196. const frame = childWindow.frameElement;
  1197. if ( frame ) {
  1198. const isFullScreen = toggleClass( frame, 'fullscreen' );
  1199. frame.ownerDocument.body.style.overflow = isFullScreen ? 'hidden' : '';
  1200. }
  1201. }
  1202. function addRemoveClass( elem, className, add ) {
  1203. if ( add ) {
  1204. addClass( elem, className );
  1205. } else {
  1206. removeClass( elem, className );
  1207. }
  1208. }
  1209. function toggleSourcePane( pressedButton ) {
  1210. forEachHTMLPart( function ( info ) {
  1211. const pressed = pressedButton === info.button;
  1212. if ( pressed && ! info.showing ) {
  1213. addClass( info.button, 'show' );
  1214. info.pane.style.display = 'flex';
  1215. info.showing = true;
  1216. } else {
  1217. removeClass( info.button, 'show' );
  1218. info.pane.style.display = 'none';
  1219. info.showing = false;
  1220. }
  1221. } );
  1222. showOtherIfAllPanesOff();
  1223. resize();
  1224. }
  1225. function showingResultPane() {
  1226. return g.result.style.display !== 'none';
  1227. }
  1228. function toggleResultPane() {
  1229. const showing = showingResultPane();
  1230. g.result.style.display = showing ? 'none' : 'block';
  1231. addRemoveClass( g.resultButton, 'show', ! showing );
  1232. showOtherIfAllPanesOff();
  1233. resize();
  1234. }
  1235. function showOtherIfAllPanesOff() {
  1236. let paneOn = showingResultPane();
  1237. forEachHTMLPart( function ( info ) {
  1238. paneOn = paneOn || info.showing;
  1239. } );
  1240. g.other.style.display = paneOn ? 'none' : 'block';
  1241. }
  1242. // seems like we should probably store a map
  1243. function getEditorNdxByBlobUrl( type, url ) {
  1244. return htmlParts[ type ].sources.findIndex( source => source.scriptInfo.blobUrl === url );
  1245. }
  1246. function getActualLineNumberAndMoveTo( url, lineNo, colNo ) {
  1247. let origUrl = url;
  1248. let actualLineNo = lineNo;
  1249. const scriptInfo = Object.values( g.scriptInfos ).find( scriptInfo => scriptInfo.blobUrl === url );
  1250. if ( scriptInfo ) {
  1251. actualLineNo = lineNo - scriptInfo.numLinesBeforeScript;
  1252. origUrl = basename( scriptInfo.fqURL );
  1253. if ( ! g.setPosition ) {
  1254. // Only set the first position
  1255. g.setPosition = true;
  1256. const editorNdx = getEditorNdxByBlobUrl( 'js', url );
  1257. if ( editorNdx >= 0 ) {
  1258. showEditorSubPane( 'js', editorNdx );
  1259. const editor = htmlParts.js.editors[ editorNdx ].editor;
  1260. editor.setPosition( {
  1261. lineNumber: actualLineNo,
  1262. column: colNo,
  1263. } );
  1264. editor.revealLineInCenterIfOutsideViewport( actualLineNo );
  1265. editor.focus();
  1266. }
  1267. }
  1268. }
  1269. return { origUrl, actualLineNo };
  1270. }
  1271. window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo;
  1272. function runEditor( parent, source, language ) {
  1273. return monaco.editor.create( parent, {
  1274. value: source,
  1275. language: language,
  1276. //lineNumbers: false,
  1277. theme: 'vs-dark',
  1278. disableTranslate3d: true,
  1279. // model: null,
  1280. scrollBeyondLastLine: false,
  1281. minimap: { enabled: false },
  1282. } );
  1283. }
  1284. async function runAsBlob() {
  1285. const query = getQuery();
  1286. g.url = getFQUrl( query.url );
  1287. g.query = getSearch( g.url );
  1288. let html;
  1289. try {
  1290. html = await getHTML( query.url );
  1291. } catch ( err ) {
  1292. console.log(err); // eslint-disable-line
  1293. return;
  1294. }
  1295. await parseHTML( query.url, html );
  1296. window.location.href = getSourceBlobFromOrig();
  1297. }
  1298. function applySubstitutions() {
  1299. [ ...document.querySelectorAll( '[data-subst]' ) ].forEach( ( elem ) => {
  1300. elem.dataset.subst.split( '&' ).forEach( ( pair ) => {
  1301. const [ attr, key ] = pair.split( '|' );
  1302. elem[ attr ] = lessonEditorSettings[ key ];
  1303. } );
  1304. } );
  1305. }
  1306. function start() {
  1307. const parentQuery = getQuery( window.parent.location.search );
  1308. const isSmallish = window.navigator.userAgent.match( /Android|iPhone|iPod|Windows Phone/i );
  1309. const isEdge = window.navigator.userAgent.match( /Edge/i );
  1310. if ( isEdge || isSmallish || parentQuery.editor === 'false' ) {
  1311. runAsBlob();
  1312. // var url = query.url;
  1313. // window.location.href = url;
  1314. } else {
  1315. applySubstitutions();
  1316. require.config( { paths: { 'vs': '/manual/3rdparty/monaco-editor/min/vs' } } );
  1317. require( [ 'vs/editor/editor.main' ], main );
  1318. }
  1319. }
  1320. start();
  1321. }() );