feeles-ide
Version:
The hackable and serializable IDE to make learning material
750 lines (664 loc) • 25.1 kB
JavaScript
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;