feeles-ide
Version:
The hackable and serializable IDE to make learning material
693 lines (619 loc) • 23.4 kB
JavaScript
import _slicedToArray from 'babel-runtime/helpers/slicedToArray';
import _Object$entries from 'babel-runtime/core-js/object/entries';
import _regeneratorRuntime from 'babel-runtime/regenerator';
import _extends from 'babel-runtime/helpers/extends';
import _JSON$stringify from 'babel-runtime/core-js/json/stringify';
import _asyncToGenerator from 'babel-runtime/helpers/asyncToGenerator';
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 _Promise from 'babel-runtime/core-js/promise';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import AppBar from 'material-ui/AppBar';
import IconButton from 'material-ui/IconButton';
import IconMenu from 'material-ui/IconMenu';
import MenuItem from 'material-ui/MenuItem';
import FlatButton from 'material-ui/FlatButton';
import Drawer from 'material-ui/Drawer';
import Snackbar from 'material-ui/Snackbar';
import CircularProgress from 'material-ui/CircularProgress';
import Toggle from 'material-ui/Toggle';
import FileDownload from 'material-ui/svg-icons/file/file-download';
import FileCloudUpload from 'material-ui/svg-icons/file/cloud-upload';
import ActionLanguage from 'material-ui/svg-icons/action/language';
import ActionHistory from 'material-ui/svg-icons/action/history';
import ActionAccountCircle from 'material-ui/svg-icons/action/account-circle';
import ActionAutorenew from 'material-ui/svg-icons/action/autorenew';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationMenu from 'material-ui/svg-icons/navigation/menu';
import ArrowDropRight from 'material-ui/svg-icons/navigation-arrow-drop-right';
import { emphasize } from 'material-ui/utils/colorManipulator';
import TwitterIcon from '../utils/TwitterIcon';
import FacebookIcon from '../utils/FacebookIcon';
import GoogleIcon from '../utils/GoogleIcon';
import { acceptedLanguages } from '../localization/';
import CloneDialog from './CloneDialog';
import MetaDialog from './MetaDialog';
import { updateProject } from '../database/';
import organization from '../organization';
import debugWindow from '../utils/debugWindow';
import open from '../utils/open';
import fetchPonyfill from 'fetch-ponyfill';
var fetch = window.fetch ||
// for IE11
fetchPonyfill({
// TODO: use babel-runtime to rewrite this into require("babel-runtime/core-js/promise")
Promise: _Promise
}).fetch;
var getStyles = function getStyles(props, context) {
var palette = context.muiTheme.palette;
return {
root: {
flex: '0 0 auto',
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
zIndex: null
},
leftIcon: {
display: props.showAll ? 'block' : 'none',
marginTop: 0
},
button: {
marginLeft: 20,
zIndex: 2
},
projectName: {
color: palette.alternateTextColor,
fontSize: '.8rem',
fontWeight: 600
},
hidden: {
opacity: 0,
width: 1,
height: 1
},
progress: {
marginLeft: 20,
padding: 12
},
visits: {
color: palette.alternateTextColor,
fontSize: '.8rem'
},
twitter: {
color: '#FFFFFF',
backgroundColor: '#1DA1F2'
},
line: {
color: '#FFFFFF',
backgroundColor: '#00C300'
},
facebook: {
color: '#FFFFFF',
backgroundColor: '#3B5998'
},
google: {
color: 'rgba(0,0,0,0.54)',
backgroundColor: '#FFFFFF'
},
toggle: {
width: 'initial',
filter: 'contrast(40%)'
},
toggleLabel: {
color: palette.alternateTextColor
}
};
};
var Menu = function (_PureComponent) {
_inherits(Menu, _PureComponent);
function Menu() {
var _ref,
_this2 = this;
var _temp, _this, _ret;
_classCallCheck(this, Menu);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Menu.__proto__ || _Object$getPrototypeOf(Menu)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
overrideTitle: null,
open: false,
isDeploying: false,
notice: null
}, _this.handleClone = function () {
_this.props.openFileDialog(CloneDialog, {
files: _this.props.files,
project: _this.props.project,
setProject: _this.props.setProject,
launchIDE: _this.props.launchIDE,
deployURL: _this.props.deployURL
});
}, _this.handleDeploy = function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(withOAuth, isUpdate) {
var localization, result, composed, actionURL, body, ogp, feelesrc, response, text, _JSON$parse, search, api, deployURL;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
localization = _this.props.localization;
_context.next = 3;
return _this.props.openFileDialog(MetaDialog, {
getConfig: _this.props.getConfig,
setConfig: _this.props.setConfig,
findFile: _this.props.findFile
});
case 3:
result = _context.sent;
if (result) {
_context.next = 6;
break;
}
return _context.abrupt('return');
case 6:
_this.setState({ isDeploying: true });
_context.prev = 7;
_context.next = 10;
return _Promise.all(_this.filesForPublishing.map(function (item) {
return item.collect();
}));
case 10:
composed = _context.sent;
// isUpdate の場合は PUT products/:search, そうでない場合は POST products
actionURL = isUpdate ? _this.props.deployURL : organization.api.deploy;
body = new URLSearchParams();
body.append('json', _JSON$stringify(composed));
body.append('script_src', 'https://unpkg.com/feeles-ide@latest/umd/index.js');
if (withOAuth) {
body.append('oauth_id', _this.props.oAuthId);
}
if (isUpdate) {
body.append('_method', 'PUT');
// Update og:url
ogp = _this.props.getConfig('ogp');
if (ogp['og:url'] !== _this.shareURL) {
_this.props.setConfig('ogp', _extends({}, ogp, {
'og:url': _this.shareURL
}));
}
}
body.append('ogp', _JSON$stringify(_this.props.getConfig('ogp')));
// .feelesrc.yml の情報を付与
feelesrc = _this.props.loadConfig('feelesrc');
if (!isUpdate && feelesrc.kitIdentifier) {
// キットの識別子 e.g. com.feeles.hack-rpg
body.append('kit_identifier', feelesrc.kitIdentifier);
}
// store/update action
_context.next = 22;
return fetch(actionURL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
// [Safari Bug] URLSearchParams not supported in bodyInit
body: body.toString(),
mode: 'cors'
});
case 22:
response = _context.sent;
if (!response.ok) {
_context.next = 37;
break;
}
_context.next = 26;
return response.text();
case 26:
text = _context.sent;
_JSON$parse = JSON.parse(text), search = _JSON$parse.search;
api = new URL(actionURL);
deployURL = api.origin + '/api/v1/products/' + search;
_this.props.setDeployURL(deployURL);
if (!_this.props.project) {
_context.next = 34;
break;
}
_context.next = 34;
return updateProject(_this.props.project.id, { deployURL: deployURL });
case 34:
_this.setState({
notice: {
message: localization.menu.published,
action: localization.menu.goToSee,
autoHideDuration: 20000,
onActionClick: function onActionClick() {
return window.open(api.origin + '/p/' + search);
}
}
});
_context.next = 39;
break;
case 37:
alert(localization.menu.failedToDeploy);
debugWindow(response);
case 39:
_context.next = 44;
break;
case 41:
_context.prev = 41;
_context.t0 = _context['catch'](7);
console.info(_context.t0);
case 44:
_this.setState({ isDeploying: false });
case 45:
case 'end':
return _context.stop();
}
}
}, _callee, _this2, [[7, 41]]);
}));
return function (_x, _x2) {
return _ref2.apply(this, arguments);
};
}(), _this.handleLogout = function () {
_this.props.setOAuthId();
_this.handleRequestClose();
}, _this.handleRequestClose = function () {
_this.setState({ notice: null });
}, _this.handleToggleDrawer = function () {
return _this.setState({
open: !_this.state.open
});
}, _this.handleSetTitle = function (event) {
_this.setState({ overrideTitle: event.data.value });
}, _temp), _possibleConstructorReturn(_this, _ret);
}
_createClass(Menu, [{
key: 'handleLoginWithOAuth',
value: function handleLoginWithOAuth(url) {
var _this3 = this;
var localization = this.props.localization;
var win = open(url);
var callback = function callback(oAuthId) {
_this3.props.setOAuthId(oAuthId);
_this3.setState({
notice: {
message: localization.menu.loggedIn,
action: localization.menu.logout,
autoHideDuration: 20000,
onActionClick: _this3.handleLogout
}
});
};
window.addEventListener('message', function task(event) {
if (event.source === win) {
window.removeEventListener('message', task);
callback(event.data.id);
}
});
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
this.props.globalEvent.on('message.menuTitle', this.handleSetTitle);
}
}, {
key: 'render',
value: function render() {
var _this4 = this;
var _props = this.props,
localization = _props.localization,
setLocalization = _props.setLocalization;
var styles = getStyles(this.props, this.context);
var alternateTextColor = this.context.muiTheme.palette.alternateTextColor;
var isLoggedin = this.props.oAuthId !== null;
var title = this.props.project && (this.props.project.title ? React.createElement(
'div',
{ style: styles.projectName },
this.props.project.title
) : React.createElement(FlatButton, {
label: localization.cloneDialog.setTitle,
labelStyle: {
color: alternateTextColor
},
onClick: this.handleClone
}));
return React.createElement(
AppBar,
{
title: this.state.overrideTitle || title,
style: styles.root,
titleStyle: { flex: null },
iconStyleLeft: styles.leftIcon,
iconElementLeft: React.createElement(
IconButton,
{ onClick: this.handleToggleDrawer },
React.createElement(NavigationMenu, null)
)
},
React.createElement('div', { style: { flex: 1 } }),
React.createElement(Toggle, {
label: this.props.showAll ? '' : localization.menu.showAll,
toggled: this.props.showAll,
onToggle: this.props.toggleShowAll,
style: styles.toggle,
labelStyle: styles.toggleLabel
}),
this.props.showAll ? React.createElement(
IconButton,
{
tooltip: localization.menu.clone,
onClick: this.handleClone,
style: styles.button
},
React.createElement(FileDownload, { color: alternateTextColor })
) : null,
this.state.isDeploying ? React.createElement(CircularProgress, {
size: 24,
style: styles.progress,
color: alternateTextColor
}) : null,
!this.state.isDeploying && this.props.showAll ? React.createElement(
IconMenu,
{
iconButtonElement: React.createElement(
IconButton,
{ tooltip: localization.menu.you },
React.createElement(ActionAccountCircle, { color: alternateTextColor })
),
anchorOrigin: {
horizontal: 'right',
vertical: 'top'
},
targetOrigin: {
horizontal: 'right',
vertical: 'bottom'
},
style: styles.button
},
isLoggedin ? null : React.createElement(MenuItem, {
primaryText: localization.menu.deployAnonymous,
leftIcon: React.createElement(FileCloudUpload, null),
onClick: function onClick() {
return _this4.handleDeploy(false, false);
}
}),
isLoggedin ? React.createElement(MenuItem, {
primaryText: localization.menu.deploySelf,
rightIcon: React.createElement(ArrowDropRight, null),
menuItems: [React.createElement(MenuItem, {
key: '1',
primaryText: localization.menu.update,
disabled: !this.props.deployURL,
leftIcon: React.createElement(ActionAutorenew, null),
onClick: function onClick() {
return _this4.handleDeploy(true, true);
}
}), React.createElement(MenuItem, {
key: '2',
primaryText: localization.menu.create,
leftIcon: React.createElement(FileCloudUpload, null),
onClick: function onClick() {
return _this4.handleDeploy(true, false);
}
})]
}) : React.createElement(MenuItem, {
primaryText: localization.menu.login,
disabled: isLoggedin,
leftIcon: React.createElement(ActionAccountCircle, null),
rightIcon: React.createElement(ArrowDropRight, null),
menuItems: [React.createElement(HoverMenuItem, {
key: '1',
primaryText: localization.menu.withGoogle,
leftIcon: React.createElement(GoogleIcon, null),
style: styles.google,
onClick: function onClick() {
return _this4.handleLoginWithOAuth(organization.api.google);
}
}), React.createElement(HoverMenuItem, {
key: '2',
primaryText: localization.menu.withFacebook,
leftIcon: React.createElement(FacebookIcon, null),
style: styles.facebook,
onClick: function onClick() {
return _this4.handleLoginWithOAuth(organization.api.facebook);
}
}), React.createElement(HoverMenuItem, {
key: '3',
primaryText: localization.menu.withTwitter,
leftIcon: React.createElement(TwitterIcon, null),
style: styles.twitter,
onClick: function onClick() {
return _this4.handleLoginWithOAuth(organization.api.twitter);
}
})]
}),
isLoggedin ? React.createElement(MenuItem, {
primaryText: localization.menu.logout,
onClick: this.handleLogout
}) : null
) : null,
React.createElement(
IconMenu,
{
iconButtonElement: React.createElement(
IconButton,
{ tooltip: localization.menu.language },
React.createElement(ActionLanguage, { color: alternateTextColor })
),
anchorOrigin: {
horizontal: 'right',
vertical: 'top'
},
targetOrigin: {
horizontal: 'right',
vertical: 'bottom'
},
style: styles.button
},
acceptedLanguages.map(function (lang) {
return React.createElement(MenuItem, {
key: lang.accept[0],
primaryText: lang.native,
onClick: function onClick() {
return setLocalization(lang.accept[0]);
}
});
})
),
React.createElement(
Drawer,
{
open: this.state.open,
docked: false,
onRequestChange: function onRequestChange(open) {
return _this4.setState({ open: open });
}
},
React.createElement(AppBar, {
iconElementLeft: React.createElement(
IconButton,
{ onClick: this.handleToggleDrawer },
React.createElement(NavigationArrowBack, null)
)
}),
this.state.open ? _Object$entries(this.props.cards).map(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
name = _ref4[0],
card = _ref4[1];
return _extends({
name: name
}, card);
}).map(function (item) {
return React.createElement(MenuItem, {
key: item.name,
primaryText: localization[lowerCaseAtFirst(item.name)].title,
leftIcon: _this4.props.cardIcons && _this4.props.cardIcons[item.name] ? _this4.props.cardIcons[item.name]() : null,
onClick: function onClick() {
_this4.props.updateCard(item.name, { visible: true });
_this4.handleToggleDrawer();
}
});
}) : null,
React.createElement(MenuItem, {
primaryText: localization.menu.version,
leftIcon: React.createElement(ActionHistory, null),
onClick: function onClick() {
_this4.handleAbout();
_this4.handleToggleDrawer();
}
})
),
React.createElement(Snackbar, _extends({
open: this.state.notice !== null,
message: '',
autoHideDuration: 4000,
style: styles.snackbar,
onRequestClose: this.handleRequestClose
}, this.state.notice))
);
}
}, {
key: 'shareURL',
get: function get() {
if (!this.props.deployURL) {
return location.href;
}
var url = new URL(this.props.deployURL);
var origin = url.origin,
pathname = url.pathname;
return origin + '/p/' + pathname.split('/').reverse()[0];
}
}, {
key: 'filesForPublishing',
get: function get() {
// i18n 設定の固定:
// 1. i18n/ll_CC/ 以下のファイルを取得
var prefix = 'i18n/' + this.props.localization.ll_CC + '/';
var currentLocales = this.props.files.filter(function (file) {
return file.name.startsWith(prefix);
});
// 2. i18n/ 以下のファイルをすべて削除し、
var withoutI18n = this.props.files.filter(function (file) {
return !file.name.startsWith('i18n/');
});
// 3. i18n/ll_CC/ 以下のファイルをルートに追加する
var intoRoot = currentLocales.map(function (file) {
var _file$name$split = file.name.split(prefix),
_file$name$split2 = _slicedToArray(_file$name$split, 2),
name = _file$name$split2[1];
return file.set({ name: name });
});
return withoutI18n.concat(intoRoot);
}
}]);
return Menu;
}(PureComponent);
Menu.propTypes = {
cards: PropTypes.object.isRequired,
cardIcons: PropTypes.object,
files: PropTypes.array.isRequired,
openFileDialog: PropTypes.func.isRequired,
localization: PropTypes.object.isRequired,
setLocalization: PropTypes.func.isRequired,
getConfig: PropTypes.func.isRequired,
setConfig: PropTypes.func.isRequired,
loadConfig: PropTypes.func.isRequired,
findFile: PropTypes.func.isRequired,
project: PropTypes.object,
setProject: PropTypes.func.isRequired,
updateCard: PropTypes.func.isRequired,
launchIDE: PropTypes.func.isRequired,
deployURL: PropTypes.string,
setDeployURL: PropTypes.func.isRequired,
oAuthId: PropTypes.string,
setOAuthId: PropTypes.func.isRequired,
showAll: PropTypes.bool.isRequired,
toggleShowAll: PropTypes.func.isRequired,
globalEvent: PropTypes.object.isRequired
};
Menu.contextTypes = {
muiTheme: PropTypes.object.isRequired
};
export default Menu;
function lowerCaseAtFirst(string) {
return string[0].toLowerCase() + string.substr(1);
}
var HoverMenuItem = function (_PureComponent2) {
_inherits(HoverMenuItem, _PureComponent2);
function HoverMenuItem() {
var _ref5;
var _temp2, _this5, _ret2;
_classCallCheck(this, HoverMenuItem);
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _ret2 = (_temp2 = (_this5 = _possibleConstructorReturn(this, (_ref5 = HoverMenuItem.__proto__ || _Object$getPrototypeOf(HoverMenuItem)).call.apply(_ref5, [this].concat(args))), _this5), _this5.state = {
hover: false
}, _temp2), _possibleConstructorReturn(_this5, _ret2);
}
_createClass(HoverMenuItem, [{
key: 'render',
value: function render() {
var _this6 = this;
var style = this.props.style;
if (this.state.hover) {
style = _extends({}, style, {
backgroundColor: emphasize(style.backgroundColor, 0.14)
});
}
return React.createElement(MenuItem, _extends({}, this.props, {
style: style,
onMouseEnter: function onMouseEnter() {
return _this6.setState({ hover: true });
},
onMouseLeave: function onMouseLeave() {
return _this6.setState({ hover: false });
}
}));
}
}]);
return HoverMenuItem;
}(PureComponent);
HoverMenuItem.propTypes = {
style: PropTypes.object.isRequired
};