editor.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. (function() {
  2. "use strict";
  3. /* global monaco, require */
  4. const lessonHelperScriptRE = /<script src="[^"]+threejs-lessons-helper\.js"><\/script>/;
  5. function getQuery(s) {
  6. s = s === undefined ? window.location.search : s;
  7. if (s[0] === '?' ) {
  8. s = s.substring(1);
  9. }
  10. var query = {};
  11. s.split('&').forEach(function(pair) {
  12. var parts = pair.split('=').map(decodeURIComponent);
  13. query[parts[0]] = parts[1];
  14. });
  15. return query;
  16. }
  17. function getSearch(url) {
  18. // yea I know this is not perfect but whatever
  19. var s = url.indexOf('?');
  20. return s < 0 ? {} : getQuery(url.substring(s));
  21. }
  22. const getFQUrl = (function() {
  23. const a = document.createElement("a");
  24. return function getFQUrl(url) {
  25. a.href = url;
  26. return a.href;
  27. };
  28. }());
  29. function getHTML(url, callback) {
  30. var req = new XMLHttpRequest();
  31. req.open("GET", url, true);
  32. req.addEventListener('load', function() {
  33. var success = req.status === 200 || req.status === 0;
  34. callback(success ? null : 'could not load: ' + url, req.responseText);
  35. });
  36. req.addEventListener('timeout', function() {
  37. callback("timeout get: " + url);
  38. });
  39. req.addEventListener('error', function() {
  40. callback("error getting: " + url);
  41. });
  42. req.send("");
  43. }
  44. function fixSourceLinks(url, source) {
  45. var srcRE = /(src=)"(.*?)"/g;
  46. var linkRE = /(href=)"(.*?")/g;
  47. var imageSrcRE = /((?:image|img)\.src = )"(.*?)"/g;
  48. var loaderLoadRE = /(loader\.load)\(('|")(.*?)('|")/g;
  49. var u = new URL(window.location.origin + url);
  50. var prefix = u.origin + dirname(u.pathname);
  51. function addPrefix(url) {
  52. return url.indexOf("://") < 0 ? (prefix + url) : url;
  53. }
  54. function makeLinkFQed(match, p1, url) {
  55. return p1 + '"' + addPrefix(url) + '"';
  56. }
  57. source = source.replace(srcRE, makeLinkFQed);
  58. source = source.replace(linkRE, makeLinkFQed);
  59. source = source.replace(imageSrcRE, makeLinkFQed);
  60. source = source.replace(loaderLoadRE, function(match, fn, q1, url, q2) {
  61. return fn + '(' + q1 + addPrefix(url) + q2;
  62. });
  63. return source;
  64. }
  65. var g = {
  66. html: '',
  67. };
  68. var htmlParts = {
  69. js: {
  70. language: 'javascript',
  71. },
  72. css: {
  73. language: 'css',
  74. },
  75. html: {
  76. language: 'html',
  77. },
  78. };
  79. function forEachHTMLPart(fn) {
  80. Object.keys(htmlParts).forEach(function(name, ndx) {
  81. var info = htmlParts[name];
  82. fn(info, ndx, name);
  83. });
  84. }
  85. function getHTMLPart(re, obj, tag) {
  86. var part = '';
  87. obj.html = obj.html.replace(re, function(p0, p1) {
  88. part = p1;
  89. return tag;
  90. });
  91. return part.replace(/\s*/, '');
  92. }
  93. function parseHTML(url, html) {
  94. html = fixSourceLinks(url, html);
  95. html = html.replace(/<div class="description">[^]*?<\/div>/, '');
  96. var styleRE = /<style>([^]*?)<\/style>/i;
  97. var titleRE = /<title>([^]*?)<\/title>/i;
  98. var bodyRE = /<body>([^]*?)<\/body>/i;
  99. var inlineScriptRE = /<script>([^]*?)<\/script>/i;
  100. var externalScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script\s*src\s*=\s*"(.*?)"\s*>\s*<\/script>/ig;
  101. var dataScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script (.*?)>([^]*?)<\/script>/ig;
  102. var cssLinkRE = /<link ([^>]+?)>/g;
  103. var isCSSLinkRE = /type="text\/css"|rel="stylesheet"/;
  104. var hrefRE = /href="([^"]+)"/;
  105. var obj = { html: html };
  106. htmlParts.css.source = getHTMLPart(styleRE, obj, '<style>\n${css}</style>');
  107. htmlParts.html.source = getHTMLPart(bodyRE, obj, '<body>${html}</body>');
  108. htmlParts.js.source = getHTMLPart(inlineScriptRE, obj, '<script>${js}</script>');
  109. html = obj.html;
  110. var tm = titleRE.exec(html);
  111. if (tm) {
  112. g.title = tm[1];
  113. }
  114. var scripts = '';
  115. html = html.replace(externalScriptRE, function(p0, p1, p2) {
  116. p1 = p1 || '';
  117. scripts += '\n' + p1 + '<script src="' + p2 + '"></script>';
  118. return '';
  119. });
  120. var dataScripts = '';
  121. html = html.replace(dataScriptRE, function(p0, p1, p2, p3) {
  122. p1 = p1 || '';
  123. dataScripts += '\n' + p1 + '<script ' + p2 + '>' + p3 + '</script>';
  124. return '';
  125. });
  126. htmlParts.html.source += dataScripts;
  127. htmlParts.html.source += scripts + '\n';
  128. // add style section if there is non
  129. if (html.indexOf("${css}") < 0) {
  130. html = html.replace("</head>", "<style>\n${css}</style>\n</head>");
  131. }
  132. // add hackedparams section.
  133. // We need a way to pass parameters to a blob. Normally they'd be passed as
  134. // query params but that only works in Firefox >:(
  135. html = html.replace("</head>", '<script id="hackedparams">window.hackedParams = ${hackedParams}\n</script>\n</head>');
  136. var links = '';
  137. html = html.replace(cssLinkRE, function(p0, p1) {
  138. if (isCSSLinkRE.test(p1)) {
  139. var m = hrefRE.exec(p1);
  140. if (m) {
  141. links += `@import url("${m[1]}");\n`;
  142. }
  143. return '';
  144. } else {
  145. return p0;
  146. }
  147. });
  148. htmlParts.css.source = links + htmlParts.css.source;
  149. g.html = html;
  150. }
  151. function cantGetHTML(e) { // eslint-disable-line
  152. console.log(e); // eslint-disable-line
  153. console.log("TODO: don't run editor if can't get HTML"); // eslint-disable-line
  154. }
  155. function main() {
  156. var query = getQuery();
  157. g.url = getFQUrl(query.url);
  158. g.query = getSearch(g.url);
  159. getHTML(query.url, function(err, html) {
  160. if (err) {
  161. console.log(err); // eslint-disable-line
  162. return;
  163. }
  164. parseHTML(query.url, html);
  165. setupEditor(query.url);
  166. if (query.startPane) {
  167. const button = document.querySelector(".button-" + query.startPane);
  168. toggleSourcePane(button);
  169. }
  170. });
  171. }
  172. var blobUrl;
  173. function getSourceBlob(options) {
  174. options = options || {};
  175. if (blobUrl) {
  176. URL.revokeObjectURL(blobUrl);
  177. }
  178. var source = g.html;
  179. source = source.replace("${hackedParams}", JSON.stringify(g.query));
  180. source = source.replace('${html}', htmlParts.html.editor.getValue());
  181. source = source.replace('${css}', htmlParts.css.editor.getValue());
  182. source = source.replace('${js}', htmlParts.js.editor.getValue());
  183. source = source.replace('<head>', '<head>\n<script match="false">threejsLessonSettings = ' + JSON.stringify(options) + ";</script>");
  184. var scriptNdx = source.indexOf('<script>');
  185. g.numLinesBeforeScript = (source.substring(0, scriptNdx).match(/\n/g) || []).length;
  186. var blob = new Blob([source], {type: 'text/html'});
  187. blobUrl = URL.createObjectURL(blob);
  188. return blobUrl;
  189. }
  190. function dirname(path) {
  191. var ndx = path.lastIndexOf("/");
  192. return path.substring(0, ndx + 1);
  193. }
  194. function resize() {
  195. forEachHTMLPart(function(info) {
  196. info.editor.layout();
  197. });
  198. }
  199. function addCORSSupport(js) {
  200. // not yet needed for three.js
  201. return js;
  202. }
  203. function openInCodepen() {
  204. const comment = `// ${g.title}
  205. // from ${g.url}
  206. `;
  207. const pen = {
  208. title : g.title,
  209. description : "from: " + g.url,
  210. tags : ["three.js", "threejsfundamentals.org"],
  211. editors : "101",
  212. html : htmlParts.html.editor.getValue().replace(lessonHelperScriptRE, ''),
  213. css : htmlParts.css.editor.getValue(),
  214. js : comment + addCORSSupport(htmlParts.js.editor.getValue()),
  215. };
  216. const elem = document.createElement("div");
  217. elem.innerHTML = `
  218. <form method="POST" target="_blank" action="https://codepen.io/pen/define" class="hidden">'
  219. <input type="hidden" name="data">
  220. <input type="submit" />
  221. "</form>"
  222. `;
  223. elem.querySelector("input[name=data]").value = JSON.stringify(pen);
  224. window.frameElement.ownerDocument.body.appendChild(elem);
  225. elem.querySelector("form").submit();
  226. window.frameElement.ownerDocument.body.removeChild(elem);
  227. }
  228. function openInJSFiddle() {
  229. const comment = `// ${g.title}
  230. // from ${g.url}
  231. `;
  232. // const pen = {
  233. // title : g.title,
  234. // description : "from: " + g.url,
  235. // tags : ["three.js", "threejsfundamentals.org"],
  236. // editors : "101",
  237. // html : htmlParts.html.editor.getValue(),
  238. // css : htmlParts.css.editor.getValue(),
  239. // js : comment + htmlParts.js.editor.getValue(),
  240. // };
  241. const elem = document.createElement("div");
  242. elem.innerHTML = `
  243. <form method="POST" target="_black" action="https://jsfiddle.net/api/mdn/" class="hidden">
  244. <input type="hidden" name="html" />
  245. <input type="hidden" name="css" />
  246. <input type="hidden" name="js" />
  247. <input type="hidden" name="title" />
  248. <input type="hidden" name="wrap" value="b" />
  249. <input type="submit" />
  250. </form>
  251. `;
  252. elem.querySelector("input[name=html]").value = htmlParts.html.editor.getValue().replace(lessonHelperScriptRE, '');
  253. elem.querySelector("input[name=css]").value = htmlParts.css.editor.getValue();
  254. elem.querySelector("input[name=js]").value = comment + addCORSSupport(htmlParts.js.editor.getValue());
  255. elem.querySelector("input[name=title]").value = g.title;
  256. window.frameElement.ownerDocument.body.appendChild(elem);
  257. elem.querySelector("form").submit();
  258. window.frameElement.ownerDocument.body.removeChild(elem);
  259. }
  260. function setupEditor() {
  261. forEachHTMLPart(function(info, ndx, name) {
  262. info.parent = document.querySelector(".panes>." + name);
  263. info.editor = runEditor(info.parent, info.source, info.language);
  264. info.button = document.querySelector(".button-" + name);
  265. info.button.addEventListener('click', function() {
  266. toggleSourcePane(info.button);
  267. run();
  268. });
  269. });
  270. g.fullscreen = document.querySelector(".button-fullscreen");
  271. g.fullscreen.addEventListener('click', toggleFullscreen);
  272. g.run = document.querySelector(".button-run");
  273. g.run.addEventListener('click', run);
  274. g.iframe = document.querySelector(".result>iframe");
  275. g.other = document.querySelector(".panes .other");
  276. document.querySelector(".button-codepen").addEventListener('click', openInCodepen);
  277. document.querySelector(".button-jsfiddle").addEventListener('click', openInJSFiddle);
  278. g.result = document.querySelector(".panes .result");
  279. g.resultButton = document.querySelector(".button-result");
  280. g.resultButton.addEventListener('click', function() {
  281. toggleResultPane();
  282. run();
  283. });
  284. g.result.style.display = "none";
  285. toggleResultPane();
  286. if (window.innerWidth > 1200) {
  287. toggleSourcePane(htmlParts.js.button);
  288. }
  289. window.addEventListener('resize', resize);
  290. showOtherIfAllPanesOff();
  291. document.querySelector(".other .loading").style.display = "none";
  292. resize();
  293. run({glDebug: false});
  294. }
  295. function toggleFullscreen() {
  296. try {
  297. toggleIFrameFullscreen(window);
  298. resize();
  299. run();
  300. } catch (e) {
  301. console.error(e); // eslint-disable-line
  302. }
  303. }
  304. function run(options) {
  305. g.setPosition = false;
  306. var url = getSourceBlob(options);
  307. g.iframe.src = url;
  308. }
  309. function addClass(elem, className) {
  310. var parts = elem.className.split(" ");
  311. if (parts.indexOf(className) < 0) {
  312. elem.className = elem.className + " " + className;
  313. }
  314. }
  315. function removeClass(elem, className) {
  316. var parts = elem.className.split(" ");
  317. var numParts = parts.length;
  318. for (;;) {
  319. var ndx = parts.indexOf(className);
  320. if (ndx < 0) {
  321. break;
  322. }
  323. parts.splice(ndx, 1);
  324. }
  325. if (parts.length !== numParts) {
  326. elem.className = parts.join(" ");
  327. return true;
  328. }
  329. return false;
  330. }
  331. function toggleClass(elem, className) {
  332. if (removeClass(elem, className)) {
  333. return false;
  334. } else {
  335. addClass(elem, className);
  336. return true;
  337. }
  338. }
  339. function toggleIFrameFullscreen(childWindow) {
  340. const frame = childWindow.frameElement;
  341. if (frame) {
  342. const isFullScreen = toggleClass(frame, "fullscreen");
  343. frame.ownerDocument.body.style.overflow = isFullScreen ? "hidden" : "";
  344. }
  345. }
  346. function addRemoveClass(elem, className, add) {
  347. if (add) {
  348. addClass(elem, className);
  349. } else {
  350. removeClass(elem, className);
  351. }
  352. }
  353. function toggleSourcePane(pressedButton) {
  354. forEachHTMLPart(function(info) {
  355. var pressed = pressedButton === info.button;
  356. if (pressed && !info.showing) {
  357. addClass(info.button, "show");
  358. info.parent.style.display = "block";
  359. info.showing = true;
  360. } else {
  361. removeClass(info.button, "show");
  362. info.parent.style.display = "none";
  363. info.showing = false;
  364. }
  365. });
  366. showOtherIfAllPanesOff();
  367. resize();
  368. }
  369. function showingResultPane() {
  370. return g.result.style.display !== "none";
  371. }
  372. function toggleResultPane() {
  373. var showing = showingResultPane();
  374. g.result.style.display = showing ? "none" : "block";
  375. addRemoveClass(g.resultButton, "show", !showing);
  376. showOtherIfAllPanesOff();
  377. resize();
  378. }
  379. function showOtherIfAllPanesOff() {
  380. var paneOn = showingResultPane();
  381. forEachHTMLPart(function(info) {
  382. paneOn = paneOn || info.showing;
  383. });
  384. g.other.style.display = paneOn ? "none" : "block";
  385. }
  386. function getActualLineNumberAndMoveTo(lineNo, colNo) {
  387. var actualLineNo = lineNo - g.numLinesBeforeScript;
  388. if (!g.setPosition) {
  389. // Only set the first position
  390. g.setPosition = true;
  391. htmlParts.js.editor.setPosition({
  392. lineNumber: actualLineNo,
  393. column: colNo,
  394. });
  395. htmlParts.js.editor.revealLineInCenterIfOutsideViewport(actualLineNo);
  396. htmlParts.js.editor.focus();
  397. }
  398. return actualLineNo;
  399. }
  400. window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo;
  401. function runEditor(parent, source, language) {
  402. return monaco.editor.create(parent, {
  403. value: source,
  404. language: language,
  405. //lineNumbers: false,
  406. theme: 'vs-dark',
  407. disableTranslate3d: true,
  408. // model: null,
  409. scrollBeyondLastLine: false,
  410. minimap: { enabled: false },
  411. });
  412. }
  413. function start() {
  414. var query = getQuery();
  415. var parentQuery = getQuery(window.parent.location.search);
  416. var isSmallish = window.navigator.userAgent.match(/Android|iPhone|iPod|Windows Phone/i);
  417. var isEdge = window.navigator.userAgent.match(/Edge/i);
  418. if (isEdge || isSmallish || parentQuery.editor === 'false') {
  419. var url = query.url;
  420. window.location.href = url;
  421. } else {
  422. require.config({ paths: { 'vs': '/monaco-editor/min/vs' }});
  423. require(['vs/editor/editor.main'], main);
  424. }
  425. }
  426. start();
  427. }());