index.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. var host = {};
  2. var view = view || {};
  3. host.BrowserHost = class {
  4. constructor() {
  5. this._document = window.document;
  6. this._window = window;
  7. this._navigator = navigator;
  8. if (this._window.location.hostname.endsWith('.github.io')) {
  9. this._window.location.replace('https://netron.app');
  10. }
  11. this._window.eval = () => {
  12. throw new Error('window.eval() not supported.');
  13. };
  14. this._meta = {};
  15. for (const element of Array.from(this._document.getElementsByTagName('meta'))) {
  16. if (element.content) {
  17. this._meta[element.name] = this._meta[element.name] || [];
  18. this._meta[element.name].push(element.content);
  19. }
  20. }
  21. this._environment = {
  22. 'type': this._meta.type ? this._meta.type[0] : 'Browser',
  23. 'version': this._meta.version ? this._meta.version[0] : null,
  24. 'date': this._meta.date ? new Date(this._meta.date[0].split(' ').join('T') + 'Z') : new Date(),
  25. };
  26. this._telemetry = this._environment.version && this._environment.version !== '0.0.0';
  27. }
  28. get window() {
  29. return this._window;
  30. }
  31. get document() {
  32. return this._document;
  33. }
  34. get version() {
  35. return this._environment.version;
  36. }
  37. get type() {
  38. return this._environment.type;
  39. }
  40. get agent() {
  41. const userAgent = this._navigator.userAgent.toLowerCase();
  42. if (userAgent.indexOf('safari') !== -1 && userAgent.indexOf('chrome') === -1) {
  43. return 'safari';
  44. }
  45. return 'any';
  46. }
  47. initialize(view) {
  48. this._view = view;
  49. return new Promise((resolve /*, reject */) => {
  50. const age = (new Date() - new Date(this._environment.date)) / ( 24 * 60 * 60 * 1000);
  51. if (age > 180) {
  52. this._message('Please update to the newest version.', 'Download', () => {
  53. const link = this.document.getElementById('logo-github').href;
  54. this.openURL(link);
  55. }, true);
  56. }
  57. else {
  58. const features = () => {
  59. const features = [ 'TextDecoder', 'TextEncoder', 'fetch', 'URLSearchParams', 'HTMLCanvasElement.prototype.toBlob' ];
  60. const supported = features.filter((feature) => {
  61. const path = feature.split('.').reverse();
  62. let item = this.window[path.pop()];
  63. while (item && path.length > 0) {
  64. item = item[path.pop()];
  65. }
  66. return !item;
  67. });
  68. if (supported.length > 0) {
  69. for (const feature of features) {
  70. this.event('Host', 'Browser', feature, 1);
  71. }
  72. this._message('Your browser is not supported.');
  73. }
  74. else {
  75. resolve();
  76. }
  77. };
  78. const telemetry = () => {
  79. if (this._telemetry) {
  80. const script = this.document.createElement('script');
  81. script.setAttribute('type', 'text/javascript');
  82. script.setAttribute('src', 'https://www.google-analytics.com/analytics.js');
  83. script.onload = () => {
  84. if (this.window.ga) {
  85. this.window.ga.l = 1 * new Date();
  86. this.window.ga('create', 'UA-54146-13', 'auto');
  87. this.window.ga('set', 'anonymizeIp', true);
  88. }
  89. features();
  90. };
  91. script.onerror = () => {
  92. features();
  93. };
  94. this.document.body.appendChild(script);
  95. }
  96. else {
  97. features();
  98. }
  99. };
  100. const consent = () => {
  101. this._message('This app uses cookies to report errors and anonymous usage information.', 'Accept', () => {
  102. this._setCookie('consent', 'yes', 30);
  103. telemetry();
  104. });
  105. };
  106. if (this._getCookie('consent')) {
  107. telemetry();
  108. }
  109. else {
  110. this._request('https://ipinfo.io/json', { 'Content-Type': 'application/json' }, 'utf-8', 2000).then((text) => {
  111. try {
  112. const json = JSON.parse(text);
  113. const countries = ['AT', 'BE', 'BG', 'HR', 'CZ', 'CY', 'DK', 'EE', 'FI', 'FR', 'DE', 'EL', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'SK', 'ES', 'SE', 'GB', 'UK', 'GR', 'EU', 'RO'];
  114. if (json && json.country && !countries.indexOf(json.country) !== -1) {
  115. this._setCookie('consent', Date.now(), 30);
  116. telemetry();
  117. }
  118. else {
  119. consent();
  120. }
  121. }
  122. catch (err) {
  123. consent();
  124. }
  125. }).catch(() => {
  126. consent();
  127. });
  128. }
  129. }
  130. });
  131. }
  132. start() {
  133. this.window.addEventListener('error', (e) => {
  134. this.exception(new Error(e ? e.message : JSON.stringify(e)), true);
  135. });
  136. const params = new URLSearchParams(this.window.location.search);
  137. this._menu = new host.Dropdown(this, 'menu-button', 'menu-dropdown');
  138. this._menu.add({
  139. label: 'Properties...',
  140. accelerator: 'CmdOrCtrl+Enter',
  141. click: () => this._view.showModelProperties()
  142. });
  143. this._menu.add({});
  144. this._menu.add({
  145. label: 'Find...',
  146. accelerator: 'CmdOrCtrl+F',
  147. click: () => this._view.find()
  148. });
  149. this._menu.add({});
  150. this._menu.add({
  151. label: () => this._view.options.attributes ? 'Hide Attributes' : 'Show Attributes',
  152. accelerator: 'CmdOrCtrl+D',
  153. click: () => this._view.toggle('attributes')
  154. });
  155. this._menu.add({
  156. label: () => this._view.options.initializers ? 'Hide Initializers' : 'Show Initializers',
  157. accelerator: 'CmdOrCtrl+I',
  158. click: () => this._view.toggle('initializers')
  159. });
  160. this._menu.add({
  161. label: () => this._view.options.names ? 'Hide Names' : 'Show Names',
  162. accelerator: 'CmdOrCtrl+U',
  163. click: () => this._view.toggle('names')
  164. });
  165. this._menu.add({
  166. label: () => this._view.options.direction === 'vertical' ? 'Show Horizontal' : 'Show Vertical',
  167. accelerator: 'CmdOrCtrl+K',
  168. click: () => this._view.toggle('direction')
  169. });
  170. this._menu.add({
  171. label: () => this._view.options.mousewheel === 'scroll' ? 'Mouse Wheel: Zoom' : 'Mouse Wheel: Scroll',
  172. accelerator: 'CmdOrCtrl+M',
  173. click: () => this._view.toggle('mousewheel')
  174. });
  175. this._menu.add({});
  176. this._menu.add({
  177. label: 'Zoom In',
  178. accelerator: 'Shift+Up',
  179. click: () => this.document.getElementById('zoom-in-button').click()
  180. });
  181. this._menu.add({
  182. label: 'Zoom Out',
  183. accelerator: 'Shift+Down',
  184. click: () => this.document.getElementById('zoom-out-button').click()
  185. });
  186. this._menu.add({
  187. label: 'Actual Size',
  188. accelerator: 'Shift+Backspace',
  189. click: () => this._view.resetZoom()
  190. });
  191. this._menu.add({});
  192. this._menu.add({
  193. label: 'Export as PNG',
  194. accelerator: 'CmdOrCtrl+Shift+E',
  195. click: () => this._view.export(document.title + '.png')
  196. });
  197. this._menu.add({
  198. label: 'Export as SVG',
  199. accelerator: 'CmdOrCtrl+Alt+E',
  200. click: () => this._view.export(document.title + '.svg')
  201. });
  202. this.document.getElementById('menu-button').addEventListener('click', (e) => {
  203. this._menu.toggle();
  204. e.preventDefault();
  205. });
  206. this._menu.add({});
  207. this._menu.add({
  208. label: 'About ' + this.document.title,
  209. click: () => this._about()
  210. });
  211. this.document.getElementById('version').innerText = this.version;
  212. if (this._meta.file) {
  213. const url = this._meta.file[0];
  214. if (this._view.accept(url)) {
  215. this._openModel(this._url(url), null);
  216. return;
  217. }
  218. }
  219. const url = params.get('url');
  220. if (url) {
  221. const identifier = params.get('identifier') || null;
  222. const location = url.replace(new RegExp('^https://github.com/([\\w]*/[\\w]*)/blob/([\\w/_.]*)(\\?raw=true)?$'), 'https://raw.githubusercontent.com/$1/$2');
  223. if (this._view.accept(identifier || location)) {
  224. this._openModel(location, identifier);
  225. return;
  226. }
  227. }
  228. const gist = params.get('gist');
  229. if (gist) {
  230. this._openGist(gist);
  231. return;
  232. }
  233. const openFileButton = this.document.getElementById('open-file-button');
  234. const openFileDialog = this.document.getElementById('open-file-dialog');
  235. if (openFileButton && openFileDialog) {
  236. openFileButton.addEventListener('click', () => {
  237. openFileDialog.value = '';
  238. openFileDialog.click();
  239. });
  240. openFileDialog.addEventListener('change', (e) => {
  241. if (e.target && e.target.files && e.target.files.length > 0) {
  242. const files = Array.from(e.target.files);
  243. const file = files.find((file) => this._view.accept(file.name));
  244. if (file) {
  245. this._open(file, files);
  246. }
  247. }
  248. });
  249. }
  250. const githubButton = this.document.getElementById('github-button');
  251. const githubLink = this.document.getElementById('logo-github');
  252. if (githubButton && githubLink) {
  253. githubButton.style.opacity = 1;
  254. githubButton.addEventListener('click', () => {
  255. this.openURL(githubLink.href);
  256. });
  257. }
  258. this.document.addEventListener('dragover', (e) => {
  259. e.preventDefault();
  260. });
  261. this.document.addEventListener('drop', (e) => {
  262. e.preventDefault();
  263. });
  264. this.document.body.addEventListener('drop', (e) => {
  265. e.preventDefault();
  266. if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  267. const files = Array.from(e.dataTransfer.files);
  268. const file = files.find((file) => this._view.accept(file.name));
  269. if (file) {
  270. this._open(file, files);
  271. }
  272. }
  273. });
  274. this._view.show('welcome');
  275. }
  276. environment(name) {
  277. return this._environment[name];
  278. }
  279. error(message, detail) {
  280. alert((message == 'Error' ? '' : message + ' ') + detail);
  281. }
  282. confirm(message, detail) {
  283. return confirm(message + ' ' + detail);
  284. }
  285. require(id) {
  286. const url = this._url(id + '.js');
  287. this.window.__modules__ = this.window.__modules__ || {};
  288. if (this.window.__modules__[url]) {
  289. return Promise.resolve(this.window.__exports__[url]);
  290. }
  291. return new Promise((resolve, reject) => {
  292. this.window.module = { exports: {} };
  293. const script = document.createElement('script');
  294. script.setAttribute('id', id);
  295. script.setAttribute('type', 'text/javascript');
  296. script.setAttribute('src', url);
  297. script.onload = (e) => {
  298. if (this.window.module && this.window.module.exports) {
  299. const exports = this.window.module.exports;
  300. delete this.window.module;
  301. this.window.__modules__[id] = exports;
  302. resolve(exports);
  303. }
  304. else {
  305. reject(new Error('The script \'' + e.target.src + '\' has no exports.'));
  306. }
  307. };
  308. script.onerror = (e) => {
  309. delete this.window.module;
  310. reject(new Error('The script \'' + e.target.src + '\' failed to load.'));
  311. };
  312. this.document.head.appendChild(script);
  313. });
  314. }
  315. save(name, extension, defaultPath, callback) {
  316. callback(defaultPath + '.' + extension);
  317. }
  318. export(file, blob) {
  319. const element = this.document.createElement('a');
  320. element.download = file;
  321. element.href = URL.createObjectURL(blob);
  322. this.document.body.appendChild(element);
  323. element.click();
  324. this.document.body.removeChild(element);
  325. }
  326. request(file, encoding, base) {
  327. const url = base ? (base + '/' + file) : this._url(file);
  328. return this._request(url, null, encoding);
  329. }
  330. openURL(url) {
  331. this.window.location = url;
  332. }
  333. exception(error, fatal) {
  334. if (this._telemetry && this.window.ga && error && error.telemetry !== false) {
  335. const description = [];
  336. description.push((error && error.name ? (error.name + ': ') : '') + (error && error.message ? error.message : JSON.stringify(error)));
  337. if (error.stack) {
  338. const format = (file, line, column) => {
  339. return file.split('\\').join('/').split('/').pop() + ':' + line + ':' + column;
  340. };
  341. const match = error.stack.match(/\n {4}at (.*) \((.*):(\d*):(\d*)\)/);
  342. if (match) {
  343. description.push(match[1] + ' (' + format(match[2], match[3], match[4]) + ')');
  344. }
  345. else {
  346. const match = error.stack.match(/\n {4}at (.*):(\d*):(\d*)/);
  347. if (match) {
  348. description.push('(' + format(match[1], match[2], match[3]) + ')');
  349. }
  350. else {
  351. const match = error.stack.match(/\n {4}at (.*)\((.*)\)/);
  352. if (match) {
  353. description.push('(' + format(match[1], match[2], match[3]) + ')');
  354. }
  355. else {
  356. const match = error.stack.match(/\s*@\s*(.*):(.*):(.*)/);
  357. if (match) {
  358. description.push('(' + format(match[1], match[2], match[3]) + ')');
  359. }
  360. else {
  361. const match = error.stack.match(/.*\n\s*(.*)\s*/);
  362. description.push(match ? match[1] : error.stack.split('\n').shift());
  363. }
  364. }
  365. }
  366. }
  367. }
  368. this.window.ga('send', 'exception', {
  369. exDescription: description.join(' @ '),
  370. exFatal: fatal,
  371. appName: this.type,
  372. appVersion: this.version
  373. });
  374. }
  375. }
  376. screen(name) {
  377. if (this._telemetry && this.window.ga) {
  378. this.window.ga('send', 'screenview', {
  379. screenName: name,
  380. appName: this.type,
  381. appVersion: this.version
  382. });
  383. }
  384. }
  385. event(category, action, label, value) {
  386. if (this._telemetry && this.window.ga) {
  387. this.window.ga('send', 'event', {
  388. eventCategory: category,
  389. eventAction: action,
  390. eventLabel: label,
  391. eventValue: value,
  392. appName: this.type,
  393. appVersion: this.version
  394. });
  395. }
  396. }
  397. _request(url, headers, encoding, timeout) {
  398. return new Promise((resolve, reject) => {
  399. const request = new XMLHttpRequest();
  400. if (!encoding) {
  401. request.responseType = 'arraybuffer';
  402. }
  403. if (timeout) {
  404. request.timeout = timeout;
  405. }
  406. const error = (status) => {
  407. const err = new Error("The web request failed with status code " + status + " at '" + url + "'.");
  408. err.type = 'error';
  409. err.url = url;
  410. return err;
  411. };
  412. request.onload = () => {
  413. if (request.status == 200) {
  414. if (request.responseType == 'arraybuffer') {
  415. resolve(new host.BrowserHost.BinaryStream(new Uint8Array(request.response)));
  416. }
  417. else {
  418. resolve(request.responseText);
  419. }
  420. }
  421. else {
  422. reject(error(request.status));
  423. }
  424. };
  425. request.onerror = (e) => {
  426. const err = error(request.status);
  427. err.type = e.type;
  428. reject(err);
  429. };
  430. request.ontimeout = () => {
  431. request.abort();
  432. const err = new Error("The web request timed out in '" + url + "'.");
  433. err.type = 'timeout';
  434. err.url = url;
  435. reject(err);
  436. };
  437. request.open('GET', url, true);
  438. if (headers) {
  439. for (const name of Object.keys(headers)) {
  440. request.setRequestHeader(name, headers[name]);
  441. }
  442. }
  443. request.send();
  444. });
  445. }
  446. _url(file) {
  447. let url = file;
  448. if (this.window && this.window.location && this.window.location.href) {
  449. let location = this.window.location.href.split('?').shift();
  450. if (location.endsWith('.html')) {
  451. location = location.split('/').slice(0, -1).join('/');
  452. }
  453. if (location.endsWith('/')) {
  454. location = location.slice(0, -1);
  455. }
  456. url = location + '/' + (file.startsWith('/') ? file.substring(1) : file);
  457. }
  458. return url;
  459. }
  460. _openModel(url, identifier) {
  461. url = url + ((/\?/).test(url) ? '&' : '?') + 'cb=' + (new Date()).getTime();
  462. this._view.show('welcome spinner');
  463. this._request(url).then((stream) => {
  464. const context = new host.BrowserHost.BrowserContext(this, url, identifier, stream);
  465. this._view.open(context).then(() => {
  466. this.document.title = identifier || context.identifier;
  467. }).catch((err) => {
  468. if (err) {
  469. this._view.error(err, null, 'welcome');
  470. }
  471. });
  472. }).catch((err) => {
  473. this.error('Model load request failed.', err.message);
  474. this._view.show('welcome');
  475. });
  476. }
  477. _open(file, files) {
  478. this._view.show('welcome spinner');
  479. const context = new host.BrowserHost.BrowserFileContext(this, file, files);
  480. context.open().then(() => {
  481. return this._view.open(context).then((model) => {
  482. this._view.show(null);
  483. this.document.title = files[0].name;
  484. return model;
  485. });
  486. }).catch((error) => {
  487. this._view.error(error, null, null);
  488. });
  489. }
  490. _openGist(gist) {
  491. this._view.show('welcome spinner');
  492. const url = 'https://api.github.com/gists/' + gist;
  493. this._request(url, { 'Content-Type': 'application/json' }, 'utf-8').then((text) => {
  494. const json = JSON.parse(text);
  495. if (json.message) {
  496. this.error('Error while loading Gist.', json.message);
  497. return;
  498. }
  499. const key = Object.keys(json.files).find((key) => this._view.accept(json.files[key].filename));
  500. if (!key) {
  501. this.error('Error while loading Gist.', 'Gist does not contain a model file.');
  502. return;
  503. }
  504. const file = json.files[key];
  505. const identifier = file.filename;
  506. const encoder = new TextEncoder();
  507. const buffer = encoder.encode(file.content);
  508. const stream = new host.BrowserHost.BinaryStream(buffer);
  509. const context = new host.BrowserHost.BrowserContext(this, '', identifier, stream);
  510. this._view.open(context).then(() => {
  511. this.document.title = identifier;
  512. }).catch((error) => {
  513. if (error) {
  514. this._view.error(error, error.name, 'welcome');
  515. }
  516. });
  517. }).catch((err) => {
  518. this._view.error(err, 'Model load request failed.', 'welcome');
  519. });
  520. }
  521. _setCookie(name, value, days) {
  522. const date = new Date();
  523. date.setTime(date.getTime() + ((typeof days !== "number" ? 365 : days) * 24 * 60 * 60 * 1000));
  524. document.cookie = name + "=" + value + ";path=/;expires=" + date.toUTCString();
  525. }
  526. _getCookie(name) {
  527. const cookie = '; ' + document.cookie;
  528. const parts = cookie.split('; ' + name + '=');
  529. return parts.length < 2 ? undefined : parts.pop().split(';').shift();
  530. }
  531. _message(message, button, callback) {
  532. const messageText = this.document.getElementById('message');
  533. if (messageText) {
  534. messageText.innerText = message;
  535. }
  536. const messageButton = this.document.getElementById('message-button');
  537. if (messageButton) {
  538. if (button && callback) {
  539. messageButton.style.removeProperty('display');
  540. messageButton.innerText = button;
  541. messageButton.onclick = () => {
  542. messageButton.onclick = null;
  543. callback();
  544. };
  545. }
  546. else {
  547. messageButton.style.display = 'none';
  548. messageButton.onclick = null;
  549. }
  550. }
  551. this._view.show('welcome message');
  552. }
  553. _about() {
  554. const self = this;
  555. const eventHandler = () => {
  556. this.window.removeEventListener('keydown', eventHandler);
  557. self.document.body.removeEventListener('click', eventHandler);
  558. self._view.show('default');
  559. };
  560. this.window.addEventListener('keydown', eventHandler);
  561. this.document.body.addEventListener('click', eventHandler);
  562. this._view.show('about');
  563. }
  564. };
  565. host.Dropdown = class {
  566. constructor(host, button, dropdown) {
  567. this._host = host;
  568. this._dropdown = this._host.document.getElementById(dropdown);
  569. this._button = this._host.document.getElementById(button);
  570. this._items = [];
  571. this._apple = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
  572. this._acceleratorMap = {};
  573. this._host.window.addEventListener('keydown', (e) => {
  574. let code = e.keyCode;
  575. code |= ((e.ctrlKey && !this._apple) || (e.metaKey && this._apple)) ? 0x0400 : 0;
  576. code |= e.altKey ? 0x0200 : 0;
  577. code |= e.shiftKey ? 0x0100 : 0;
  578. if (code == 0x001b) { // Escape
  579. this.close();
  580. return;
  581. }
  582. const item = this._acceleratorMap[code.toString()];
  583. if (item) {
  584. item.click();
  585. e.preventDefault();
  586. }
  587. });
  588. this._host.document.body.addEventListener('click', (e) => {
  589. if (!this._button.contains(e.target)) {
  590. this.close();
  591. }
  592. });
  593. }
  594. add(item) {
  595. const accelerator = item.accelerator;
  596. if (accelerator) {
  597. let cmdOrCtrl = false;
  598. let alt = false;
  599. let shift = false;
  600. let key = '';
  601. for (const part of item.accelerator.split('+')) {
  602. switch (part) {
  603. case 'CmdOrCtrl': cmdOrCtrl = true; break;
  604. case 'Alt': alt = true; break;
  605. case 'Shift': shift = true; break;
  606. default: key = part; break;
  607. }
  608. }
  609. if (key !== '') {
  610. item.accelerator = {};
  611. item.accelerator.text = '';
  612. if (this._apple) {
  613. item.accelerator.text += alt ? '&#x2325;' : '';
  614. item.accelerator.text += shift ? '&#x21e7;' : '';
  615. item.accelerator.text += cmdOrCtrl ? '&#x2318;' : '';
  616. const keyTable = { 'Enter': '&#x23ce;', 'Up': '&#x2191;', 'Down': '&#x2193;', 'Backspace': '&#x232B;' };
  617. item.accelerator.text += keyTable[key] ? keyTable[key] : key;
  618. }
  619. else {
  620. const list = [];
  621. if (cmdOrCtrl) {
  622. list.push('Ctrl');
  623. }
  624. if (alt) {
  625. list.push('Alt');
  626. }
  627. if (shift) {
  628. list.push('Shift');
  629. }
  630. list.push(key);
  631. item.accelerator.text = list.join('+');
  632. }
  633. let code = 0;
  634. switch (key) {
  635. case 'Backspace': code = 0x08; break;
  636. case 'Enter': code = 0x0D; break;
  637. case 'Up': code = 0x26; break;
  638. case 'Down': code = 0x28; break;
  639. default: code = key.charCodeAt(0); break;
  640. }
  641. code |= cmdOrCtrl ? 0x0400 : 0;
  642. code |= alt ? 0x0200 : 0;
  643. code |= shift ? 0x0100 : 0;
  644. this._acceleratorMap[code.toString()] = item;
  645. }
  646. }
  647. this._items.push(item);
  648. }
  649. toggle() {
  650. if (this._dropdown.style.display === 'block') {
  651. this.close();
  652. return;
  653. }
  654. while (this._dropdown.lastChild) {
  655. this._dropdown.removeChild(this._dropdown.lastChild);
  656. }
  657. for (const item of this._items) {
  658. if (Object.keys(item).length > 0) {
  659. const button = this._host.document.createElement('button');
  660. button.innerText = (typeof item.label == 'function') ? item.label() : item.label;
  661. button.addEventListener('click', () => {
  662. this.close();
  663. setTimeout(() => {
  664. item.click();
  665. }, 10);
  666. });
  667. this._dropdown.appendChild(button);
  668. if (item.accelerator) {
  669. const accelerator = this._host.document.createElement('span');
  670. accelerator.style.float = 'right';
  671. accelerator.innerHTML = item.accelerator.text;
  672. button.appendChild(accelerator);
  673. }
  674. }
  675. else {
  676. const separator = this._host.document.createElement('div');
  677. separator.setAttribute('class', 'separator');
  678. this._dropdown.appendChild(separator);
  679. }
  680. }
  681. this._dropdown.style.display = 'block';
  682. }
  683. close() {
  684. this._dropdown.style.display = 'none';
  685. }
  686. };
  687. host.BrowserHost.BinaryStream = class {
  688. constructor(buffer) {
  689. this._buffer = buffer;
  690. this._length = buffer.length;
  691. this._position = 0;
  692. }
  693. get position() {
  694. return this._position;
  695. }
  696. get length() {
  697. return this._length;
  698. }
  699. stream(length) {
  700. const buffer = this.read(length);
  701. return new host.BrowserHost.BinaryStream(buffer.slice(0));
  702. }
  703. seek(position) {
  704. this._position = position >= 0 ? position : this._length + position;
  705. }
  706. skip(offset) {
  707. this._position += offset;
  708. }
  709. peek(length) {
  710. if (this._position === 0 && length === undefined) {
  711. return this._buffer;
  712. }
  713. const position = this._position;
  714. this.skip(length !== undefined ? length : this._length - this._position);
  715. const end = this._position;
  716. this.seek(position);
  717. return this._buffer.subarray(position, end);
  718. }
  719. read(length) {
  720. if (this._position === 0 && length === undefined) {
  721. this._position = this._length;
  722. return this._buffer;
  723. }
  724. const position = this._position;
  725. this.skip(length !== undefined ? length : this._length - this._position);
  726. return this._buffer.subarray(position, this._position);
  727. }
  728. byte() {
  729. const position = this._position;
  730. this.skip(1);
  731. return this._buffer[position];
  732. }
  733. };
  734. host.BrowserHost.BrowserFileContext = class {
  735. constructor(host, file, blobs) {
  736. this._host = host;
  737. this._file = file;
  738. this._blobs = {};
  739. for (const blob of blobs) {
  740. this._blobs[blob.name] = blob;
  741. }
  742. }
  743. get identifier() {
  744. return this._file.name;
  745. }
  746. get stream() {
  747. return this._stream;
  748. }
  749. request(file, encoding, base) {
  750. if (base !== undefined) {
  751. return this._host.request(file, encoding, base);
  752. }
  753. const blob = this._blobs[file];
  754. if (!blob) {
  755. return Promise.reject(new Error("File not found '" + file + "'."));
  756. }
  757. return new Promise((resolve, reject) => {
  758. const reader = new FileReader();
  759. reader.onload = (e) => {
  760. resolve(encoding ? e.target.result : new host.BrowserHost.BinaryStream(new Uint8Array(e.target.result)));
  761. };
  762. reader.onerror = (e) => {
  763. e = e || this.window.event;
  764. let message = '';
  765. const error = e.target.error;
  766. switch(error.code) {
  767. case error.NOT_FOUND_ERR:
  768. message = "File not found '" + file + "'.";
  769. break;
  770. case error.NOT_READABLE_ERR:
  771. message = "File not readable '" + file + "'.";
  772. break;
  773. case error.SECURITY_ERR:
  774. message = "File access denied '" + file + "'.";
  775. break;
  776. default:
  777. message = error.message ? error.message : "File read '" + error.code.toString() + "' error '" + file + "'.";
  778. break;
  779. }
  780. reject(new Error(message));
  781. };
  782. if (encoding === 'utf-8') {
  783. reader.readAsText(blob, encoding);
  784. }
  785. else {
  786. reader.readAsArrayBuffer(blob);
  787. }
  788. });
  789. }
  790. require(id) {
  791. return this._host.require(id);
  792. }
  793. exception(error, fatal) {
  794. this._host.exception(error, fatal);
  795. }
  796. open() {
  797. return this.request(this._file.name, null).then((stream) => {
  798. this._stream = stream;
  799. });
  800. }
  801. };
  802. host.BrowserHost.BrowserContext = class {
  803. constructor(host, url, identifier, stream) {
  804. this._host = host;
  805. this._stream = stream;
  806. if (identifier) {
  807. this._identifier = identifier;
  808. this._base = url;
  809. if (this._base.endsWith('/')) {
  810. this._base.substring(0, this._base.length - 1);
  811. }
  812. }
  813. else {
  814. const parts = url.split('?')[0].split('/');
  815. this._identifier = parts.pop();
  816. this._base = parts.join('/');
  817. }
  818. }
  819. get identifier() {
  820. return this._identifier;
  821. }
  822. get stream() {
  823. return this._stream;
  824. }
  825. request(file, encoding, base) {
  826. return this._host.request(file, encoding, base === undefined ? this._base : base);
  827. }
  828. require(id) {
  829. return this._host.require(id);
  830. }
  831. exception(error, fatal) {
  832. this._host.exception(error, fatal);
  833. }
  834. };
  835. if (!('scrollBehavior' in window.document.documentElement.style)) {
  836. const __scrollTo__ = Element.prototype.scrollTo;
  837. Element.prototype.scrollTo = function(options) {
  838. if (options === undefined) {
  839. return;
  840. }
  841. if (options === null || typeof options !== 'object' || options.behavior === undefined || arguments[0].behavior === 'auto' || options.behavior === 'instant') {
  842. if (__scrollTo__) {
  843. __scrollTo__.apply(this, arguments);
  844. }
  845. return;
  846. }
  847. const now = () => {
  848. return window.performance && window.performance.now ? window.performance.now() : Date.now();
  849. };
  850. const ease = (k) => {
  851. return 0.5 * (1 - Math.cos(Math.PI * k));
  852. };
  853. const step = (context) => {
  854. const value = ease(Math.min((now() - context.startTime) / 468, 1));
  855. const x = context.startX + (context.x - context.startX) * value;
  856. const y = context.startY + (context.y - context.startY) * value;
  857. context.element.scrollLeft = x;
  858. context.element.scrollTop = y;
  859. if (x !== context.x || y !== context.y) {
  860. window.requestAnimationFrame(step.bind(window, context));
  861. }
  862. };
  863. const context = {
  864. element: this,
  865. x: typeof options.left === 'undefined' ? this.scrollLeft : ~~options.left,
  866. y: typeof options.top === 'undefined' ? this.scrollTop : ~~options.top,
  867. startX: this.scrollLeft,
  868. startY: this.scrollTop,
  869. startTime: now()
  870. };
  871. step(context);
  872. };
  873. }
  874. window.addEventListener('load', () => {
  875. window.__host__ = new host.BrowserHost();
  876. window.__view__ = new view.View(window.__host__);
  877. });