UNPKG

feeles-ide

Version:

The hackable and serializable IDE to make learning material

750 lines (664 loc) 25.1 kB
import _regeneratorRuntime from 'babel-runtime/regenerator'; import _asyncToGenerator from 'babel-runtime/helpers/asyncToGenerator'; import _Set from 'babel-runtime/core-js/set'; import _Object$entries from 'babel-runtime/core-js/object/entries'; import _extends from 'babel-runtime/helpers/extends'; import _Promise from 'babel-runtime/core-js/promise'; import _Object$assign from 'babel-runtime/core-js/object/assign'; import _toConsumableArray from 'babel-runtime/helpers/toConsumableArray'; import _slicedToArray from 'babel-runtime/helpers/slicedToArray'; import _getIterator from 'babel-runtime/core-js/get-iterator'; import _JSON$stringify from 'babel-runtime/core-js/json/stringify'; import _Map from 'babel-runtime/core-js/map'; import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of'; import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; import _createClass from 'babel-runtime/helpers/createClass'; import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; import _inherits from 'babel-runtime/helpers/inherits'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import EventEmitter from 'eventemitter2'; import Snackbar from 'material-ui/Snackbar'; import jsyaml from 'js-yaml'; import _ from 'lodash'; var tryParseYAML = function tryParseYAML(text) { var defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; try { return jsyaml.safeLoad(text); } catch (e) { console.info(e); return defaultValue; } }; var tryParseJSON = function tryParseJSON(text) { var defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; try { return JSON.parse(text); } catch (e) { console.info(e); return defaultValue; } }; import { FileView, createProject, updateProject } from '../database/'; import { SourceFile, configs } from '../File/'; import codemirrorStyle from '../js/codemirrorStyle'; import * as MonitorTypes from '../utils/MonitorTypes'; import Menu from '../Menu/'; import FileDialog, { SaveDialog } from '../FileDialog/'; import cardStateDefault from '../Cards/defaultState'; import CardContainer from '../Cards/CardContainer'; import CloneDialog from '../Menu/CloneDialog'; import Footer from './Footer'; var DOWNLOAD_ENABLED = typeof document.createElement('a').download === 'string'; var getStyle = function getStyle(props, state, context) { var shrinkLeft = parseInt(props.rootStyle.width, 10) - state.monitorWidth < 200; var shrinkRight = state.monitorWidth < 100; var palette = context.muiTheme.palette; return { shrinkLeft: shrinkLeft, shrinkRight: shrinkRight, root: { position: 'relative', width: '100%', height: '100%', display: 'flex', flexDirection: 'column', lineHeight: 1.15, backgroundColor: palette.backgroundColor } }; }; var Main = function (_Component) { _inherits(Main, _Component); function Main() { var _ref; var _temp, _this, _ret; _classCallCheck(this, Main); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Main.__proto__ || _Object$getPrototypeOf(Main)).call.apply(_ref, [this].concat(args))), _this), _this.state = { monitorType: MonitorTypes.Card, fileView: new FileView(_this.props.files), reboot: false, href: 'index.html', tabs: [], project: _this.props.project, notice: null, // OAuth 認証によって得られる UUID. // あくまで発行しているのは feeles.com である // この値はユーザが見えるところには表示してはならない oAuthId: null, cards: cardStateDefault, cardIcons: null, // Advanced Mode showAll: false, // card =(emit)=> globalEvent =(on)=> card globalEvent: new EventEmitter({ wildcard: true }) }, _this._configs = new _Map(), _this.getConfig = function (key) { if (_this._configs.has(key)) { return _this._configs.get(key); } else { var _configs$get = configs.get(key), test = _configs$get.test, defaultValue = _configs$get.defaultValue, multiple = _configs$get.multiple, bundle = _configs$get.bundle; var files = _this.findFile(function (file) { return !file.options.isTrashed && test.test(file.name); }, multiple); var value = files ? multiple ? bundle(files) : files.json : defaultValue; _this._configs.set(key, value); return value; } }, _this.setConfig = function (key, config) { _this._configs.set(key, config); var _configs$get2 = configs.get(key), test = _configs$get2.test, defaultName = _configs$get2.defaultName; var configFile = _this.findFile(function (file) { return !file.options.isTrashed && test.test(file.name); }, false); // Update Mui theme if (key === 'palette') { var feelesrc = _this.loadConfig('feelesrc'); feelesrc.palette = config; _this.props.setMuiTheme(feelesrc); } var indent = ' '; var text = _JSON$stringify(config, null, indent); if (configFile) { return _this.putFile(configFile, configFile.set({ text: text })); } else { var newFile = new SourceFile({ type: 'application/json', name: defaultName, text: text }); return _this.addFile(newFile); } }, _this.resetConfig = function (fileName) { // Refresh config var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = _getIterator(configs.entries()), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var _step$value = _slicedToArray(_step.value, 2), key = _step$value[0], value = _step$value[1]; if (value.test.test(fileName)) { _this._configs.delete(key); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } }, _this.loadConfig = function (ext) { var json = ext + '.json'; var yaml = ext + '.yml'; // TODO: オブジェクト(ハッシュ)以外も使えるようにする var values = [].concat( // .json (JSON) _this.state.fileView.getFilesByExtention(json).map(function (file) { return tryParseJSON(file.text, {}); }), // .yml (YAML) _this.state.fileView.getFilesByExtention(yaml).map(function (file) { return tryParseYAML(file.text, {}); }), // .(ext) (JSON) _this.state.fileView.getFilesByExtention(ext).map(function (file) { return tryParseJSON(file.text, {}); })); return _Object$assign.apply(Object, [{}].concat(_toConsumableArray(values))); }, _this.selectTab = function (tab) { return new _Promise(function (resolve) { var tabs = _this.state.tabs.map(function (item) { if (item.isSelected) return item.select(false); return item; }); var found = tabs.find(function (item) { return item.is(tab); }); if (found) { var replace = found.select(true); _this.setState({ tabs: tabs.map(function (item) { return item === found ? replace : item; }) }, function () { return resolve(replace); }); } else { if (!tab.isSelected) tab = tab.select(true); _this.setState({ tabs: tabs.concat(tab) }, function () { return resolve(tab); }); } _this.updateCard('EditorCard', { visible: true }); }); }, _this.closeTab = function (tab) { return new _Promise(function (resolve) { var tabs = _this.state.tabs.filter(function (item) { return item.key !== tab.key; }); if (tab.isSelected && tabs.length > 0) { tabs[0] = tabs[0].select(true); } _this.setState({ tabs: tabs }, function () { return resolve(); }); }); }, _this.saveAs = function () { for (var _len2 = arguments.length, files = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { files[_key2] = arguments[_key2]; } if (DOWNLOAD_ENABLED) { files.forEach(function (file) { var a = document.createElement('a'); var event = document.createEvent('MouseEvents'); event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); a.download = file.name; a.href = file.blobURL || URL.createObjectURL(new Blob([file.text], { type: file.type })); a.dispatchEvent(event); if (a.href !== file.blobURL) { URL.revokeObjectURL(a.href); } }); return _Promise.resolve(); } else { // for Safari/IE11/Edge return _this.openFileDialog(SaveDialog, { content: files }); } }, _this.setProject = function (project) { return _this.setStatePromise({ project: project }); }, _this.handleTogglePopout = function () { var isPopout = _this.state.monitorType === MonitorTypes.Popout; _this.setState({ reboot: true, monitorType: isPopout ? MonitorTypes.Card : MonitorTypes.Popout }); }, _this.handleToggleFullScreen = function () { var isFullScreen = _this.state.monitorType === MonitorTypes.FullScreen; _this.setState({ monitorType: isFullScreen ? MonitorTypes.Card : MonitorTypes.FullScreen }); }, _this.setLocation = function (href) { _this.setState(function (prevState) { return { reboot: true, href: href || prevState.href }; }); }, _this.updateCard = function (name, props) { var nextCard = _extends({}, _this.state.cards); nextCard[name] = _extends({}, nextCard[name], props); return _this.setStatePromise({ cards: nextCard }); }, _this.handleShowNotice = function (notice) { return _this.setStatePromise({ notice: notice }); }, _this.setOAuthId = function () { var oAuthId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; return _this.setStatePromise({ oAuthId: oAuthId }); }, _this.toggleShowAll = function () { return _this.setStatePromise({ showAll: !_this.state.showAll }); }, _this.openFileDialog = function () { return console.info('openFileDialog has not be declared'); }, _this.handleFileDialog = function (ref) { return ref && (_this.openFileDialog = ref.open); }, _this.handleContainerRef = function (ref) { if (!_this.state.cardIcons && ref) { var cardIcons = {}; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = _getIterator(_Object$entries(ref.cardRefs)), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _step2$value = _slicedToArray(_step2.value, 2), name = _step2$value[0], instance = _step2$value[1]; if (instance.constructor.icon) { cardIcons[name] = instance.constructor.icon; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } _this.setState({ cardIcons: cardIcons }); } }, _temp), _possibleConstructorReturn(_this, _ret); } _createClass(Main, [{ key: 'componentWillMount', value: function componentWillMount() { // 互換性保持のため、 fileView に外から setState させる this.state.fileView.install(this); var feelesrc = this.loadConfig('feelesrc'); this.props.setMuiTheme(feelesrc); var card = this.findFile('feeles/card.json'); if (card) { var cards = _.merge(this.state.cards, card.json); this.setState({ cards: cards }); } } }, { key: 'componentDidMount', value: function componentDidMount() { var _this2 = this; document.title = this.getConfig('ogp')['og:title'] || ''; // 定期的にスクリーンショットを撮る if (this.props.onThumbnailChange) { var globalEvent = this.state.globalEvent; var cache = new _Set(); globalEvent.on('message.capture', function (event) { var _ref2 = event.data || {}, value = _ref2.value; if (!cache.has(value)) { _this2.props.onThumbnailChange(value); cache.add(value); } }); var screenShotLater = function () { var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() { var request; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return new _Promise(function (resolve) { return window.setTimeout(resolve, 10 * 1000); }); case 2: request = { query: 'capture', type: 'image/jpeg' }; _context.next = 5; return _this2.state.globalEvent.emitAsync('postMessage', request); case 5: screenShotLater(); case 6: case 'end': return _context.stop(); } } }, _callee, _this2); })); return function screenShotLater() { return _ref3.apply(this, arguments); }; }(); screenShotLater(); } if (this.props.onChange) { // ファイルの内容を伝える(一番最初) this.props.onChange({ files: this.props.files }); } // iframe からのイベントを伝える this.state.globalEvent.on('message.dispatchOnMessage', function (event) { if (_this2.props.onMessage) { _this2.props.onMessage(_extends({}, event)); } }); } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { if (this.props.project !== nextProps.project) { this.setProject(nextProps.project); } if (this.props.localization !== nextProps.localization) { this.state.fileView.forceUpdate(); this.setState({ reboot: true }); } } }, { key: 'componentDidUpdate', value: function () { var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(prevProps, prevState) { var _this3 = this; var localization, project, deployURL, nextProject; return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: localization = this.props.localization; if (this.state.reboot) { this.setState({ reboot: false }); } // ファイル変更検知 if (this.props.onChange && prevState.fileView !== this.state.fileView) { this.props.onChange({ files: this.state.fileView.files }); } // 未オートセーブでファイルが更新されたとき、あらたにセーブデータを作る if (!(!this.state.project && prevState.fileView !== this.state.fileView)) { _context2.next = 30; break; } if (!(process.env.NODE_ENV !== 'production')) { _context2.next = 6; break; } return _context2.abrupt('return'); case 6: if (!this.props.disableLocalSave) { _context2.next = 8; break; } return _context2.abrupt('return'); case 8: _context2.prev = 8; _context2.next = 11; return createProject(this.state.fileView.files.map(function (item) { return item.serialize(); })); case 11: project = _context2.sent; // add deployURL if exists deployURL = this.props.deployURL; if (!deployURL) { _context2.next = 21; break; } _context2.next = 16; return updateProject(project.id, { deployURL: deployURL }); case 16: nextProject = _context2.sent; _context2.next = 19; return this.setProject(nextProject); case 19: _context2.next = 23; break; case 21: _context2.next = 23; return this.setProject(project); case 23: _context2.next = 29; break; case 25: _context2.prev = 25; _context2.t0 = _context2['catch'](8); console.log(_context2.t0); if (typeof _context2.t0 === 'string' && _context2.t0 in localization.cloneDialog) { alert(localization.cloneDialog[_context2.t0]); } case 29: // notice this.setState({ notice: { message: localization.cloneDialog.autoSaved, action: localization.cloneDialog.setTitle, autoHideDuration: 20000, onActionClick: function onActionClick() { _this3.openFileDialog(CloneDialog, { files: _this3.state.fileView.files, project: _this3.state.project, setProject: _this3.setProject, launchIDE: _this3.props.launchIDE, deployURL: _this3.props.deployURL }); } } }); case 30: document.title = this.getConfig('ogp')['og:title'] || ''; case 31: case 'end': return _context2.stop(); } } }, _callee2, this, [[8, 25]]); })); function componentDidUpdate(_x4, _x5) { return _ref4.apply(this, arguments); } return componentDidUpdate; }() }, { key: 'componentWillUnmount', value: function componentWillUnmount() { this.state.fileView.uninstall(); } }, { key: 'setStatePromise', value: function () { var _ref5 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(state) { var _this4 = this; return _regeneratorRuntime.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: return _context3.abrupt('return', new _Promise(function (resolve) { _this4.setState(state, resolve); })); case 1: case 'end': return _context3.stop(); } } }, _callee3, this); })); function setStatePromise(_x6) { return _ref5.apply(this, arguments); } return setStatePromise; }() }, { key: 'render', value: function render() { var _this5 = this; var _props = this.props, localization = _props.localization, disableLocalSave = _props.disableLocalSave; var styles = getStyle(this.props, this.state, this.context); var commonProps = { fileView: this.state.fileView, files: this.state.fileView.files, localization: localization, getConfig: this.getConfig, setConfig: this.setConfig, loadConfig: this.loadConfig, findFile: this.findFile, addFile: this.addFile, putFile: this.putFile, showAll: this.state.showAll }; var userStyle = this.findFile('feeles/codemirror.css'); return React.createElement( 'div', { style: styles.root }, disableLocalSave ? null : React.createElement(Menu, _extends({}, commonProps, { setLocalization: this.props.setLocalization, openFileDialog: this.openFileDialog, saveAs: this.saveAs, project: this.state.project, setProject: this.setProject, cards: this.state.cards, updateCard: this.updateCard, launchIDE: this.props.launchIDE, deployURL: this.props.deployURL, setDeployURL: this.props.setDeployURL, oAuthId: this.state.oAuthId, setOAuthId: this.setOAuthId, showAll: this.state.showAll, toggleShowAll: this.toggleShowAll, cardIcons: this.state.cardIcons, globalEvent: this.state.globalEvent })), React.createElement(CardContainer, _extends({}, commonProps, { cards: this.state.cards, updateCard: this.updateCard, tabs: this.state.tabs, selectTab: this.selectTab, closeTab: this.closeTab, setLocation: this.setLocation, openFileDialog: this.openFileDialog, reboot: this.state.reboot, href: this.state.href, monitorType: this.state.monitorType, saveAs: this.saveAs, toggleFullScreen: this.handleToggleFullScreen, togglePopout: this.handleTogglePopout, showNotice: this.handleShowNotice, deleteFile: this.deleteFile, oAuthId: this.state.oAuthId, ref: this.handleContainerRef, globalEvent: this.state.globalEvent, disableScreenShotCard: this.props.disableScreenShotCard })), React.createElement(Footer, { deployURL: this.props.deployURL, localization: localization, showNotice: this.handleShowNotice }), React.createElement(FileDialog, { ref: this.handleFileDialog, localization: this.props.localization, getConfig: this.getConfig, setConfig: this.setConfig, globalEvent: this.state.globalEvent }), React.createElement( 'style', null, codemirrorStyle(this.context.muiTheme) ), userStyle ? React.createElement( 'style', null, userStyle.text ) : null, React.createElement(Snackbar, _extends({ open: this.state.notice !== null, message: '', autoHideDuration: 4000, onRequestClose: function onRequestClose() { return _this5.setState({ notice: null }); } }, this.state.notice)) ); } }, { key: 'rootWidth', get: function get() { return parseInt(this.props.rootStyle.width, 10); } }, { key: 'rootHeight', get: function get() { return parseInt(this.props.rootStyle.height, 10); } }]); return Main; }(Component); Main.propTypes = { files: PropTypes.array.isRequired, rootStyle: PropTypes.object.isRequired, project: PropTypes.object, launchIDE: PropTypes.func.isRequired, localization: PropTypes.object.isRequired, setLocalization: PropTypes.func.isRequired, setMuiTheme: PropTypes.func.isRequired, deployURL: PropTypes.string, setDeployURL: PropTypes.func.isRequired, onChange: PropTypes.func, onMessage: PropTypes.func, onThumbnailChange: PropTypes.func, disableLocalSave: PropTypes.bool.isRequired, disableScreenShotCard: PropTypes.bool.isRequired }; Main.contextTypes = { muiTheme: PropTypes.object.isRequired }; export default Main;