From 16ec9ec90bbb3b3c986a18a98edee8ab974f10d9 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 2 Dec 2018 19:27:22 +0300 Subject: [PATCH] Update to ES6 and Webpack --- .babelrc | 13 +---- .eslintrc.js | 45 +++++++++++++++ AccountFolders.js | 37 ++++++++----- AllDropdowns.js | 21 ++++--- AttachList.js | 63 +++++++++++---------- ComposeWindow.js | 106 +++++++++++++++++------------------ DropDownBase.js | 109 +++++++++++++++++++----------------- DropDownButton.js | 43 +++++++------- DropDownMenu.js | 39 +++++++------ FolderList.js | 41 +++++++------- I18n.js | 2 +- ListSortSettings.js | 9 +-- ListSortSettingsWindow.js | 26 ++++----- ListWithSelection.js | 43 ++++++++------ MailProgress.js | 6 +- MailSettingsWindow.js | 30 +++++----- MessageList.js | 114 +++++++++++++++++++++----------------- MessageView.js | 40 ++++++------- ProgressBar.js | 35 ++++++------ Store.js | 12 ++-- StoreListener.js | 54 +++++++++++------- StoreListenerClass.js | 52 ----------------- StoreListenerWrapper.js | 50 ----------------- TabPanel.js | 30 +++++----- Util.js | 99 +++++++++++++++++---------------- mail.css | 9 +++ mail.js | 28 +++++----- mail.js.htm | 8 +-- package.json | 42 +++++++------- webpack.config.js | 54 ++++++++++++++++++ 30 files changed, 667 insertions(+), 593 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 StoreListenerClass.js delete mode 100644 StoreListenerWrapper.js create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc index 3a3201e..b26d6e6 100644 --- a/.babelrc +++ b/.babelrc @@ -1,15 +1,4 @@ { - "plugins": [ - "check-es2015-constants", - "transform-es2015-arrow-functions", - "transform-es2015-block-scoping", - "transform-es2015-classes", - "transform-es2015-for-of", - "transform-es2015-computed-properties", - "transform-es2015-destructuring", - "transform-es2015-shorthand-properties", - "transform-object-rest-spread", - "transform-react-jsx", - ], + "presets": [ "env", "stage-1", "react" ], "retainLines": true } diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..811d3b2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + "parser": "babel-eslint", + "env": { + "es6": true, + "browser": true + }, + "globals": { + "APPCFG": false + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + } + }, + "plugins": [ + "react" + ], + "rules": { + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "semi": [ + "error", + "always" + ], + "no-control-regex": [ + "off" + ], + "no-empty": [ + "off" + ] + } +}; diff --git a/AccountFolders.js b/AccountFolders.js index 3865cb6..f0f8c86 100644 --- a/AccountFolders.js +++ b/AccountFolders.js @@ -1,8 +1,16 @@ -const React = require('react'); -const DropDownBase = require('./DropDownBase.js'); +import React from 'react'; +import DropDownBase from './DropDownBase.js'; -var AccountFolders = module.exports = React.createClass({ - render: function() +export default class AccountFolders extends React.PureComponent +{ + state = { + collapsed: this.props.collapsed, + animating: false, + h: null, + cfgPressed: false, + } + + render() { return
@@ -28,8 +36,9 @@ var AccountFolders = module.exports = React.createClass({
- }, - selectFolder: function(ev) + } + + selectFolder = (ev) => { var t = ev.target; while (t && !t.getAttribute('data-i') && t != this.refs.vis) @@ -40,8 +49,9 @@ var AccountFolders = module.exports = React.createClass({ this.props.onSelect(this.props.accountIndex, i); } // FIXME: send select event + switch focus to message list if folder changed - }, - showCfg: function(ev) + } + + showCfg = (ev) => { var self = this; var i = DropDownBase.instances.account.state.items; @@ -53,12 +63,9 @@ var AccountFolders = module.exports = React.createClass({ }); self.setState({ cfgPressed: true }); ev.stopPropagation(); - }, - getInitialState: function() - { - return { collapsed: this.props.collapsed, animating: false, h: null, cfgPressed: false }; - }, - onClick: function() + } + + onClick = () => { var self = this; if (this.state.animating) @@ -76,4 +83,4 @@ var AccountFolders = module.exports = React.createClass({ self.setState({ collapsed: !self.state.collapsed, animating: false, h: null }); }, this.state.collapsed ? 200 : 250); } -}); +} diff --git a/AllDropdowns.js b/AllDropdowns.js index 891e36a..d37e789 100644 --- a/AllDropdowns.js +++ b/AllDropdowns.js @@ -1,11 +1,12 @@ -const React = require('react'); -const DropDownMenu = require('./DropDownMenu.js'); -const ListSortSettingsWindow = require('./ListSortSettingsWindow.js'); -const MailSettingsWindow = require('./MailSettingsWindow.js'); +import React from 'react'; +import DropDownMenu from './DropDownMenu.js'; +import ListSortSettingsWindow from './ListSortSettingsWindow.js'; +import MailSettingsWindow from './MailSettingsWindow.js'; var dropdown_account = React.createElement( DropDownMenu, { id: 'account', + key: 'account', items: [ { icon: 'mail_unread', i16: true, @@ -23,6 +24,7 @@ var dropdown_account = React.createElement( var dropdown_reply = React.createElement( DropDownMenu, { id: 'reply', + key: 'reply', items: [ { hotkey: 'R', icon: 'mail_reply', @@ -41,6 +43,7 @@ var dropdown_reply = React.createElement( var dropdown_forward = React.createElement( DropDownMenu, { id: 'forward', + key: 'forward', items: [ { hotkey: 'F', icon: 'mail_forward', @@ -55,6 +58,7 @@ var dropdown_forward = React.createElement( var dropdown_delete = React.createElement( DropDownMenu, { id: 'delete', + key: 'delete', items: [ { text: 'Move to Trash' }, { @@ -67,6 +71,7 @@ var dropdown_delete = React.createElement( var dropdown_check_send = React.createElement( DropDownMenu, { id: 'check-send', + key: 'check-send', items: [ { hotkey: 'Ctrl-K', icon: 'mail_check', @@ -90,6 +95,7 @@ var dropdown_check_send = React.createElement( var dropdown_threads = React.createElement( DropDownMenu, { id: 'threads', + key: 'threads', items: [ { icon: 'thread', text: 'Show Message Thread' @@ -111,6 +117,7 @@ var dropdown_threads = React.createElement( var dropdown_list_sort = React.createElement( ListSortSettingsWindow, { id: 'list-sort', + key: 'list-sort', window: true, folder: 'INBOX', override: false, @@ -134,9 +141,7 @@ var dropdown_list_sort = React.createElement( } ); -var dropdown_settings = MailSettingsWindow; - -module.exports = function() +export default function() { return [ dropdown_account, @@ -146,6 +151,6 @@ module.exports = function() dropdown_check_send, dropdown_threads, dropdown_list_sort, - dropdown_settings + , ]; } diff --git a/AttachList.js b/AttachList.js index 1db8c3d..df9c666 100644 --- a/AttachList.js +++ b/AttachList.js @@ -1,17 +1,17 @@ -const React = require('react'); -const ListWithSelection = require('./ListWithSelection.js'); -const Util = require('./Util.js'); +import React from 'react'; -var AttachList = module.exports = React.createClass({ - mixins: [ ListWithSelection ], - getInitialState: function() - { - return { - attachments: [], - attachScroll: 0 - }; - }, - addAttachments: function(ev) +import ListWithSelection from './ListWithSelection.js'; +import Util from './Util.js'; + +export default class AttachList extends ListWithSelection +{ + state = { + ...this.state, + attachments: [], + attachScroll: 0 + } + + addAttachments = (ev) => { var a = this.state.attachments; if (ev.target.files) @@ -20,36 +20,43 @@ var AttachList = module.exports = React.createClass({ this.setState({ attachments: a }); // reset file input ev.target.innerHTML = ev.target.innerHTML; - }, - scrollAttachList: function(ev) + } + + scrollAttachList = (ev) => { this.setState({ attachScroll: ev.target.scrollTop }); - }, - deleteSelected: function() + } + + deleteSelected = () => { for (var i = this.state.attachments.length-1; i >= 0; i--) if (this.state.selected[i]) this.state.attachments.splice(i, 1); this.setState({ attachments: this.state.attachments }); - }, - getTotalItems: function() + } + + getTotalItems = () => { return this.state.attachments.length; - }, - getPageSize: function() + } + + getPageSize = () => { return this.refs.a0 ? Math.round(this.refs.scroll.offsetHeight / this.refs.a0.offsetHeight) : 1; - }, - getItemOffset: function(index) + } + + getItemOffset = (index) => { var item = this.refs['a'+index]; return [ item.offsetTop, item.offsetHeight ]; - }, - getScrollPaddingTop: function() + } + + getScrollPaddingTop = () => { return this.refs.title.offsetHeight; - }, - render: function() + } + + render() { return
@@ -74,4 +81,4 @@ var AttachList = module.exports = React.createClass({
} -}); +} diff --git a/ComposeWindow.js b/ComposeWindow.js index b1576f2..e0d9fd8 100644 --- a/ComposeWindow.js +++ b/ComposeWindow.js @@ -1,63 +1,63 @@ -const React = require('react'); -const AttachList = require('./AttachList.js'); -const StoreListener = require('./StoreListener.js'); +import React from 'react'; +import AttachList from './AttachList.js'; +import StoreListener from './StoreListener.js'; -var ComposeWindow = React.createClass({ - getInitialState: function() - { - return { - text: '' - }; - }, - changeText: function(ev) +class ComposeWindow extends React.PureComponent +{ + state = { + text: '' + } + + changeText = (ev) => { this.setState({ text: ev.target.value }); - }, - render: function() + } + + render() { - return
-
- Send - -
-
-
-
-
-
From
-
- + return (
+
+ Send + +
+
+
+
+
+
From
+
+ +
+
+
+
To
+
+
+
+
Cc
+
+
+
+
Bcc
+
+
+
+
Subject
+
-
-
To
-
-
-
-
Cc
-
-
-
-
Bcc
-
-
-
-
Subject
-
-
+ +
+
+
-
-
- -
-
-
+
); } -}); +} -module.exports = StoreListener(ComposeWindow, (data) => { return { accounts: data.accounts }; }); +export default StoreListener(ComposeWindow, (data) => { return { accounts: data.accounts }; }); diff --git a/DropDownBase.js b/DropDownBase.js index b8ce57c..6ce8923 100644 --- a/DropDownBase.js +++ b/DropDownBase.js @@ -1,35 +1,13 @@ -function getOffset(elem) -{ - if (elem.getBoundingClientRect) - { - var box = elem.getBoundingClientRect(); - var body = document.body; - var docElem = document.documentElement; - var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; - var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; - var clientTop = docElem.clientTop || body.clientTop || 0; - var clientLeft = docElem.clientLeft || body.clientLeft || 0; - var top = box.top + scrollTop - clientTop; - var left = box.left + scrollLeft - clientLeft; - return { top: Math.round(top), left: Math.round(left) }; - } - else - { - var top = 0, left = 0; - while(elem) - { - top = top + parseInt(elem.offsetTop); - left = left + parseInt(elem.offsetLeft); - elem = elem.offsetParent; - } - return { top: top, left: left }; - } -} +import React from 'react'; -var DropDownBase = module.exports = { - instances: {}, - currentVisible: null, - componentDidMount: function() +export default class DropDownBase extends React.PureComponent +{ + static instances = {}; + static currentVisible = null; + + state = { visible: false, top: 0, left: 0, calloutLeft: null, selectedItem: -1 }; + + componentDidMount() { if (!DropDownBase.setBodyListener) { @@ -38,36 +16,38 @@ var DropDownBase = module.exports = { DropDownBase.setBodyListener = true; } DropDownBase.instances[this.props.id] = this; - }, - hideAll: function() + } + + static hideAll() { for (var i in DropDownBase.instances) DropDownBase.instances[i].hide(); - }, - repositionCurrent: function() + } + + static repositionCurrent() { if (DropDownBase.currentVisible) DropDownBase.currentVisible[0].showAt(DropDownBase.currentVisible[1], DropDownBase.currentVisible[0].onClose); - }, - componentWillUnmount: function() + } + + componentWillUnmount() { delete DropDownBase.instances[this.props.id]; if (DropDownBase.currentVisible[0] == this) DropDownBase.currentVisible = null; - }, - getInitialState: function() - { - return { visible: false, top: 0, left: 0, calloutLeft: null, selectedItem: -1 }; - }, - onClick: function(ev) + } + + onClick = (ev) => { ev.stopPropagation(); - }, - isVisible: function() + } + + isVisible = () => { return this.state.visible; - }, - hide: function() + } + + hide = () => { this.setState({ visible: false }); DropDownBase.currentVisible = null; @@ -76,8 +56,9 @@ var DropDownBase = module.exports = { this.onClose(); delete this.onClose; } - }, - showAt: function(el, onClose) + } + + showAt = (el, onClose) => { if (this.onClose && this.onClose != onClose) { @@ -108,4 +89,32 @@ var DropDownBase = module.exports = { this.refs.dd.focus(); this.onClose = onClose; } -}; +} + +function getOffset(elem) +{ + if (elem.getBoundingClientRect) + { + var box = elem.getBoundingClientRect(); + var body = document.body; + var docElem = document.documentElement; + var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; + var clientTop = docElem.clientTop || body.clientTop || 0; + var clientLeft = docElem.clientLeft || body.clientLeft || 0; + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + return { top: Math.round(top), left: Math.round(left) }; + } + else + { + var top = 0, left = 0; + while(elem) + { + top = top + parseInt(elem.offsetTop); + left = left + parseInt(elem.offsetLeft); + elem = elem.offsetParent; + } + return { top: top, left: left }; + } +} diff --git a/DropDownButton.js b/DropDownButton.js index 4570bde..6dff8b9 100644 --- a/DropDownButton.js +++ b/DropDownButton.js @@ -1,16 +1,21 @@ -const React = require('react'); -const DropDownBase = require('./DropDownBase.js'); +import React from 'react'; -var DropDownButton = module.exports = React.createClass({ - componentDidUpdate: function(prevProps, prevState) +import DropDownBase from './DropDownBase.js'; + +export default class DropDownButton extends React.PureComponent +{ + state = { pressed: false, checked: false } + + componentDidUpdate(prevProps, prevState) { if (prevProps.hidden && !this.props.hidden && DropDownBase.instances[this.props.dropdownId].isVisible()) { DropDownBase.instances[this.props.dropdownId].showAt(this.refs.btn, this.unpress); } - }, - render: function() + } + + render() { return : null} - }, - getInitialState: function() - { - return { pressed: false, checked: false }; - }, - toggle: function() + } + + toggle = () => { if (!this.state.pressed) { @@ -35,12 +37,14 @@ var DropDownButton = module.exports = React.createClass({ else DropDownBase.instances[this.props.dropdownId].hide(); this.setState({ pressed: !this.state.pressed }); - }, - unpress: function() + } + + unpress = () => { this.setState({ pressed: false }); - }, - onClickButton: function(ev) + } + + onClickButton = (ev) => { if (this.props.whole || this.props.checkable && this.state.pressed) this.toggle(); @@ -53,10 +57,11 @@ var DropDownButton = module.exports = React.createClass({ else if (this.props.onClick) this.props.onClick(); ev.stopPropagation(); - }, - onClickDown: function(ev) + } + + onClickDown = (ev) => { this.toggle(); ev.stopPropagation(); } -}); +} diff --git a/DropDownMenu.js b/DropDownMenu.js index e063b26..3018646 100644 --- a/DropDownMenu.js +++ b/DropDownMenu.js @@ -1,13 +1,12 @@ -const React = require('react'); -const DropDownBase = require('./DropDownBase.js'); +import React from 'react'; -var DropDownMenu = module.exports = React.createClass({ - mixins: [ DropDownBase ], - getInitialState: function() - { - return { items: this.props.items }; - }, - render: function() +import DropDownBase from './DropDownBase.js'; + +export default class DropDownMenu extends DropDownBase +{ + state = { items: this.props.items } + + render() { var sel = this.state.selectedItem; return
- }, - onMouseOver: function(ev) + } + + onMouseOver = (ev) => { var t = ev.target; while ((t && t != this.refs.dd) && (!t.className || t.className.substr(0, 4) != 'item')) t = t.parentNode; if (t && t != this.refs.dd) this.setState({ selectedItem: parseInt(t.getAttribute('data-index')) }); - }, - onKeyDown: function(ev) + } + + onKeyDown = (ev) => { if (ev.keyCode == 40 || ev.keyCode == 38) { @@ -50,14 +51,16 @@ var DropDownMenu = module.exports = React.createClass({ this.clickItem(); ev.preventDefault(); ev.stopPropagation(); - }, - clickItem: function(ev) + } + + clickItem = (ev) => { - }, - myOnClick: function(ev) + } + + myOnClick = (ev) => { if (ev.target.getAttribute('data-index')) this.clickItem(); this.onClick(ev); } -}); +} diff --git a/FolderList.js b/FolderList.js index 2a2280f..c7f05f8 100644 --- a/FolderList.js +++ b/FolderList.js @@ -1,15 +1,18 @@ -const React = require('react'); -const AccountFolders = require('./AccountFolders.js'); -const DropDownButton = require('./DropDownButton.js'); -const Store = require('./Store.js'); -const StoreListener = require('./StoreListenerClass.js'); -const MailProgress = require('./MailProgress.js'); +import React from 'react'; +import AccountFolders from './AccountFolders.js'; +import DropDownButton from './DropDownButton.js'; +import Store from './Store.js'; +import StoreListener from './StoreListener.js'; +import MailProgress from './MailProgress.js'; -var FolderList = React.createClass({ - render: function() +class FolderList extends React.PureComponent +{ + state = { selectedAccount: -1, selectedFolder: -1 } + + render() { var self = this; - return
+ return (
@@ -24,13 +27,15 @@ var FolderList = React.createClass({ })}
-
- }, - onClickCheckSend: function() +
) + } + + onClickCheckSend = () => { Store.startResync(); - }, - onSelectFolder: function(accIndex, folderIndex) + } + + onSelectFolder = (accIndex, folderIndex) => { var acc = this.props.accounts[accIndex]; var folder = this.props.accounts[accIndex].folders[folderIndex]; @@ -42,11 +47,7 @@ var FolderList = React.createClass({ Store.loadFolder({ accountId: acc.accountId, folderType: folder.type }); } this.setState({ selectedAccount: accIndex, selectedFolder: folderIndex }); - }, - getInitialState: function() - { - return { selectedAccount: -1, selectedFolder: -1 }; } -}); +} -module.exports = StoreListener(FolderList, (data) => { return { accounts: data.accounts, progressText: data.progressText }; }); +export default StoreListener(FolderList, (data) => { return { accounts: data.accounts, progressText: data.progressText }; }); diff --git a/I18n.js b/I18n.js index c51757e..8561c14 100644 --- a/I18n.js +++ b/I18n.js @@ -1,4 +1,4 @@ -module.exports = function(msg) +export default function(msg) { return msg; } diff --git a/ListSortSettings.js b/ListSortSettings.js index f356d58..0f497ff 100644 --- a/ListSortSettings.js +++ b/ListSortSettings.js @@ -1,7 +1,8 @@ -const React = require('react'); +import React from 'react'; -var ListSortSettings = module.exports = React.createClass({ - render: function() +export default class ListSortSettings extends React.PureComponent +{ + render() { return
Threaded
} -}); +} diff --git a/ListSortSettingsWindow.js b/ListSortSettingsWindow.js index 393bdf5..d645277 100644 --- a/ListSortSettingsWindow.js +++ b/ListSortSettingsWindow.js @@ -1,10 +1,13 @@ -const React = require('react'); -const DropDownBase = require('./DropDownBase.js'); -const ListSortSettings = require('./ListSortSettings.js'); +import React from 'react'; -var ListSortSettingsWindow = module.exports = React.createClass({ - mixins: [ DropDownBase ], - render: function() +import DropDownBase from './DropDownBase.js'; +import ListSortSettings from './ListSortSettings.js'; + +export default class ListSortSettingsWindow extends DropDownBase +{ + state = { checksVisible: false } + + render() { var sort = this.props.override ? this.props.sorting : this.props.defaultSorting; return
Show Duplicates
- }, - getInitialState: function() - { - return { checksVisible: false }; - }, - expandChecks: function() + } + + expandChecks = () => { this.setState({ checksVisible: !this.state.checksVisible }); } -}); +} diff --git a/ListWithSelection.js b/ListWithSelection.js index a02aaf8..3f49bb9 100644 --- a/ListWithSelection.js +++ b/ListWithSelection.js @@ -1,18 +1,24 @@ -// Common selection mixin -var ListWithSelection = module.exports = { +import React from 'react'; + +// Common "list with selection" component + +export default class ListWithSelection extends React.PureComponent +{ // requires to override methods: this.deleteSelected(), this.getPageSize(), this.getItemOffset(index), this.getTotalItems() - getInitialState: function() + constructor(props) { - return { - selected: {} - }; - }, - isSelected: function(i) + super(props); + this.state = this.state||{}; + this.state.selected = {}; + } + + isSelected(i) { return this.state.selected[i] || this.state.selected.begin !== undefined && this.state.selected.begin <= i && this.state.selected.end >= i; - }, - onListKeyDown: function(ev) + } + + onListKeyDown = (ev) => { if (!this.getTotalItems()) return; @@ -74,8 +80,9 @@ var ListWithSelection = module.exports = { else this.selectOne(nsel); } - }, - selectTo: function(ns) + } + + selectTo(ns) { if (this.lastSel === undefined) return this.selectOne(ns); @@ -93,8 +100,9 @@ var ListWithSelection = module.exports = { this.curSel = ns; if (this.onSelectCurrent) this.onSelectCurrent(ns); - }, - selectOne: function(ns) + } + + selectOne(ns) { var sel = {}; sel[ns] = true; @@ -103,8 +111,9 @@ var ListWithSelection = module.exports = { this.curSel = ns; if (this.onSelectCurrent) this.onSelectCurrent(ns); - }, - onListItemClick: function(ev) + } + + onListItemClick = (ev) => { var t = ev.target; while (t && !t.getAttribute('data-i')) @@ -127,4 +136,4 @@ var ListWithSelection = module.exports = { this.selectOne(ns); } } -}; +} diff --git a/MailProgress.js b/MailProgress.js index 4c6c3fc..6c44811 100644 --- a/MailProgress.js +++ b/MailProgress.js @@ -1,7 +1,7 @@ -const StoreListener = require('./StoreListenerClass.js'); -const ProgressBar = require('./ProgressBar.js'); +import StoreListener from './StoreListener.js'; +import ProgressBar from './ProgressBar.js'; -module.exports = StoreListener(ProgressBar, function(data) +export default StoreListener(ProgressBar, function(data) { return { text: data.progressText, progress: data.progressPercent }; }); diff --git a/MailSettingsWindow.js b/MailSettingsWindow.js index d4e18f6..64c9cf0 100644 --- a/MailSettingsWindow.js +++ b/MailSettingsWindow.js @@ -1,12 +1,12 @@ -const React = require('react'); -const DropDownBase = require('./DropDownBase.js'); -const ListSortSettings = require('./ListSortSettings.js'); -const Store = require('./Store.js'); -const StoreListener = require('./StoreListener.js'); +import React from 'react'; +import DropDownBase from './DropDownBase.js'; +import ListSortSettings from './ListSortSettings.js'; +import Store from './Store.js'; +import StoreListener from './StoreListener.js'; -var MailSettingsWindow = React.createClass({ - mixins: [ DropDownBase ], - render: function() +class MailSettingsWindow extends DropDownBase +{ + render() { return
@@ -35,20 +35,22 @@ var MailSettingsWindow = React.createClass({
- }, - switchLayout: function(ev) + } + + switchLayout = (ev) => { var t = ev.target.nodeName == 'A' ? ev.target : ev.target.parentNode; var l = / mail-(\S+)/.exec(t.className)[1]; Store.set('layout', l); - }, - showQuickReply: function() + } + + showQuickReply = () => { Store.set('quickReply', !this.props.quickReply); } -}); +} -module.exports = StoreListener( +export default StoreListener( MailSettingsWindow, (data) => { return { layout: data.layout, quickReply: data.quickReply }; }, { diff --git a/MessageList.js b/MessageList.js index 5b472f1..33cfd47 100644 --- a/MessageList.js +++ b/MessageList.js @@ -1,13 +1,15 @@ -const React = require('react'); -const DropDownButton = require('./DropDownButton.js'); -const ListWithSelection = require('./ListWithSelection.js'); -const Store = require('./Store.js'); -const StoreListener = require('./StoreListener.js'); -const Util = require('./Util.js'); +import React from 'react'; +import DropDownButton from './DropDownButton.js'; +import ListWithSelection from './ListWithSelection.js'; +import Store from './Store.js'; +import StoreListener from './StoreListener.js'; +import Util from './Util.js'; -var MessageInList = React.createClass({ - msgClasses: { unread: 'unread', unseen: 'unseen', answered: 'replied', flagged: 'pinned', sent: 'sent' }, - render: function() +class MessageInList extends React.PureComponent +{ + msgClasses = { unread: 'unread', unseen: 'unseen', answered: 'replied', flagged: 'pinned', sent: 'sent' } + + render() { var msg = this.props.msg; return
} -}); +} // TODO: expand/collapse days -var MessageList = React.createClass({ - mixins: [ ListWithSelection ], - _preloadSize: 20, - _pageSize: 50, - getInitialState: function() - { - return { - firstDayTop: 0, - firstDay: this.props.groups && this.props.groups[0] && this.props.groups[0].name || null - }; - }, - componentWillReceiveProps: function(nextProps) +class MessageList extends ListWithSelection +{ + _preloadSize = 20 + _pageSize = 50 + state = { + ...this.state, + firstDayTop: 0, + firstDay: this.props.groups && this.props.groups[0] && this.props.groups[0].name || null + } + + componentWillReceiveProps(nextProps) { this.setFirstDayFromProps(nextProps); - }, + } + // Main virtual scroll detector method - setFirstDayFromProps: function(props) + setFirstDayFromProps(props) { var groups = props.groups; var messages = props.messages; @@ -118,16 +120,19 @@ var MessageList = React.createClass({ Store.loadMessages(loadFirst, loadFirstEnd-loadFirst); if (loadFirstEnd < loadLastStart && loadLastStart < loadLast) Store.loadMessages(loadLastStart, loadLast-loadLastStart); - }, - changeFirstDay: function(ev) + } + + changeFirstDay = (ev) => { this.setFirstDayFromProps(this.props); - }, - deleteSelected: function() + } + + deleteSelected = () => { - }, - onSelectCurrent: function(index) + } + + onSelectCurrent = (index) => { var self = this; var total = 0, p, msg, idx; @@ -155,8 +160,9 @@ var MessageList = React.createClass({ break; } } - }, - getTotalItems: function() + } + + getTotalItems = () => { var total = -1; // do not count first-day as item for (var i = 0; i < (this.props.groups||[]).length; i++) @@ -164,12 +170,14 @@ var MessageList = React.createClass({ total += 1+this.props.groups[i].messageCount; } return total; - }, - getPageSize: function() + } + + getPageSize = () => { return Math.floor(this.refs.scroll.offsetHeight / (this.props.layout == 'message-on-right' ? 60 : 30)); - }, - getItemOffset: function(index) + } + + getItemOffset = (index) => { var n = 0, top = 0, p; var h = (this.props.layout == 'message-on-right' ? 60 : 30); @@ -185,28 +193,32 @@ var MessageList = React.createClass({ top += (i > 0 ? 30 : 0) + h*this.props.groups[i].messageCount; } return [ top, index == p && i > 0 ? 30 : h ]; - }, - getScrollPaddingTop: function() + } + + getScrollPaddingTop = () => { return this.refs.title.offsetHeight; - }, - getMessages: function(grp, start, end) + } + + getMessages = (grp, start, end) => { var a = this.props.messages.slice(grp.start+start, grp.start+end); for (var i = 0; i < end-start; i++) if (!a[i]) a[i] = null; return a; - }, - onSearchTextChange: function(event) + } + + onSearchTextChange = (event) => { var s = event.target.value; this.setState({ searchText: s }); if (this._searchTimeout) clearTimeout(this._searchTimeout); this._searchTimeout = setTimeout(function() { Store.search(s) }, 300); - }, - render: function() + } + + render() { var self = this; var total = 0; @@ -270,18 +282,20 @@ var MessageList = React.createClass({ })}
- }, - componentDidMount: function() + } + + componentDidMount() { window.addEventListener('resize', this.changeFirstDay); - }, - componentWillUnmount: function() + } + + componentWillUnmount() { window.removeEventListener('resize', this.changeFirstDay); } -}); +} -module.exports = StoreListener(MessageList, function(data) +export default StoreListener(MessageList, function(data) { return { threads: data.threads, diff --git a/MessageView.js b/MessageView.js index b737ae9..339b9f4 100644 --- a/MessageView.js +++ b/MessageView.js @@ -1,11 +1,14 @@ -const React = require('react'); -const DropDownButton = require('./DropDownButton.js'); -const Store = require('./Store.js'); -const StoreListener = require('./StoreListener.js'); -const Util = require('./Util.js'); +import React from 'react'; +import DropDownButton from './DropDownButton.js'; +import Store from './Store.js'; +import StoreListener from './StoreListener.js'; +import Util from './Util.js'; -var MessageView = React.createClass({ - formatLongDate: function(dt) +class MessageView extends React.PureComponent +{ + state = { showImages: false } + + formatLongDate(dt) { if (!(dt instanceof Date)) dt = new Date(dt.replace(' ', 'T')); @@ -14,8 +17,9 @@ var MessageView = React.createClass({ var s = dt.getSeconds(); return Util.WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Util.Months[dt.getMonth()]+' '+dt.getFullYear()+' '+(h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m+':'+(s < 10 ? '0' : '')+s //return dt.toLocaleString(); - }, - componentWillReceiveProps: function(nextProps) + } + + componentWillReceiveProps(nextProps) { if (nextProps.msg != this.props.msg) { @@ -26,16 +30,14 @@ var MessageView = React.createClass({ } this.setState(ns); } - }, - showImages: function() + } + + showImages = () => { this.setState({ showImages: true }); - }, - getInitialState: function() - { - return { showImages: false }; - }, - render: function() + } + + render() { var showImages = this.state.showImages; var msg = this.props.msg; @@ -148,6 +150,6 @@ var MessageView = React.createClass({ ] : null}
} -}); +} -module.exports = StoreListener(MessageView, (data) => { return { layout: data.layout, quickReply: data.quickReply, msg: data.msg }; }); +export default StoreListener(MessageView, (data) => { return { layout: data.layout, quickReply: data.quickReply, msg: data.msg }; }); diff --git a/ProgressBar.js b/ProgressBar.js index 7177e04..97d0949 100644 --- a/ProgressBar.js +++ b/ProgressBar.js @@ -1,7 +1,10 @@ -const React = require('react'); +import React from 'react'; -var ProgressBar = module.exports = React.createClass({ - render: function() +export default class ProgressBar extends React.PureComponent +{ + state = { width: '' } + + render() { return
{this.props.text} ({this.props.progress||0}%)
@@ -9,29 +12,29 @@ var ProgressBar = module.exports = React.createClass({
{this.props.text} ({this.props.progress||0}%)
- }, - componentDidUpdate: function(prevProps, prevState) + } + + componentDidUpdate(prevProps, prevState) { if (!prevState.width) { setTimeout(this.onResize, 50); } - }, - getInitialState: function() - { - return { width: '' }; - }, - onResize: function() + } + + onResize = () => { this.setState({ width: this.refs.pbar.offsetWidth }); - }, - componentDidMount: function() + } + + componentDidMount() { window.addEventListener('resize', this.onResize); this.onResize(); - }, - componentWillUnmount: function() + } + + componentWillUnmount() { window.removeEventListener('resize', this.onResize); } -}); +} diff --git a/Store.js b/Store.js index e7cc51f..d61d6b7 100644 --- a/Store.js +++ b/Store.js @@ -1,10 +1,10 @@ -const superagent = require('superagent'); -const socket_io = require('socket.io-client'); +import superagent from 'superagent'; +import socket_io from 'socket.io-client'; -const _ = require('./I18n.js'); -const Util = require('./Util.js'); +import _ from './I18n.js'; +import Util from './Util.js'; -var Store = module.exports = { +const Store = { data: { layout: 'message-on-right', quickReply: true, @@ -195,3 +195,5 @@ var Store = module.exports = { }; Store.startIo(); + +export default Store; diff --git a/StoreListener.js b/StoreListener.js index b34403b..990b943 100644 --- a/StoreListener.js +++ b/StoreListener.js @@ -1,19 +1,23 @@ -const React = require('react'); -const Store = require('./Store.js'); +import React from 'react'; + +import Store from './Store.js'; // "react-redux connect()"-like example -var StoreListener = React.createClass({ - componentDidMount: function() +class StoreListener extends React.PureComponent +{ + componentDidMount() { Store.on(this.update); - }, - componentWillUnmount: function() + } + + componentWillUnmount() { Store.un(this.update); - }, - update: function() + } + + update = () => { - var newState = this.props.mapStateToProps(Store.data); + var newState = this.mapStateToProps(Store.data); for (var i in newState) { if (this.state[i] != newState[i]) @@ -22,18 +26,28 @@ var StoreListener = React.createClass({ return; } } - }, - getInitialState: function() - { - return { ...this.props.initial, ...this.props.mapStateToProps(Store.data) }; - }, - render: function() - { - return React.createElement(this.props.wrappedComponent, this.state); } -}); -module.exports = function(component, map, initial) + render() + { + var props = { ...this.initial, ...this.props, ...this.state }; + return React.createElement(this.wrappedComponent, props); + } +} + +export default function(component, map, initial) { - return React.createElement(StoreListener, { wrappedComponent: component, mapStateToProps: map, initial: initial||{} }); + var cl = class extends StoreListener + { + constructor(props, context, updater) + { + super(props, context, updater); + this.wrappedComponent = component; + this.mapStateToProps = map; + this.initial = initial; + this.state = map(Store.data); + this.update = this.update.bind(this); + } + }; + return cl; }; diff --git a/StoreListenerClass.js b/StoreListenerClass.js deleted file mode 100644 index e910b86..0000000 --- a/StoreListenerClass.js +++ /dev/null @@ -1,52 +0,0 @@ -const React = require('react'); -const Store = require('./Store.js'); - -// "react-redux connect()"-like example -class StoreListener extends React.Component -{ - componentDidMount() - { - Store.on(this.update); - } - - componentWillUnmount() - { - Store.un(this.update); - } - - update() - { - var newState = this.mapStateToProps(Store.data); - for (var i in newState) - { - if (this.state[i] != newState[i]) - { - this.setState(newState); - return; - } - } - } - - render() - { - var props = { ...this.initial, ...this.props, ...this.state }; - return React.createElement(this.wrappedComponent, props); - } -} - -module.exports = function(component, map, initial) -{ - var cl = class extends StoreListener - { - constructor(props, context, updater) - { - super(props, context, updater); - this.wrappedComponent = component; - this.mapStateToProps = map; - this.initial = initial; - this.state = map(Store.data); - this.update = this.update.bind(this); - } - }; - return cl; -}; diff --git a/StoreListenerWrapper.js b/StoreListenerWrapper.js deleted file mode 100644 index a9d5dd9..0000000 --- a/StoreListenerWrapper.js +++ /dev/null @@ -1,50 +0,0 @@ -// НЕ РАБОТАЕТ! -const React = require('react'); -const Store = require('./Store.js'); - -// "react-redux connect()"-like example -var StoreListener = React.createClass({ - componentDidMount: function() - { - Store.on(this.update); - }, - componentWillUnmount: function() - { - Store.un(this.update); - }, - update: function() - { - var newState = this.mapStateToProps(Store.data); - for (var i in newState) - { - if (this.state[i] != newState[i]) - { - this.setState(newState); - return; - } - } - }, - getInitialState: function() - { - return { ...this.initial, ...this.mapStateToProps(Store.data) }; - }, - render: function() - { - var props = { ...this.initial, ...this.props, ...this.state }; - return React.createElement(this.wrappedComponent, props); - } -}); - -module.exports = function(component, map, initial) -{ - var fn = function(props, context, updater) - { - StoreListener.call(this, props, context, updater); - this.wrappedComponent = component; - this.mapStateToProps = map; - this.initial = initial||{}; - }; - fn.prototype = Object.create(StoreListener); - fn.prototype.constructor = fn; - return fn; -}; diff --git a/TabPanel.js b/TabPanel.js index 94afc49..dc8b833 100644 --- a/TabPanel.js +++ b/TabPanel.js @@ -1,7 +1,10 @@ -const React = require('react'); +import React from 'react'; -var TabPanel = module.exports = React.createClass({ - render: function() +export default class TabPanel extends React.PureComponent +{ + state = { selected: 0, tabs: this.props.tabs } + + render() { var bar = []; var body = []; @@ -26,21 +29,20 @@ var TabPanel = module.exports = React.createClass({
{bar}
{body} - }, - componentWillReceiveProps: function(nextProps, nextContent) + } + + componentWillReceiveProps(nextProps, nextContent) { // FIXME: Do not own tabs? this.setState({ selected: this.state.selected % nextProps.tabs.length, tabs: nextProps.tabs }); - }, - getInitialState: function() - { - return { selected: 0, tabs: this.props.tabs }; - }, - switchTab: function(ev) + } + + switchTab = (ev) => { this.setState({ selected: ev.target.id.substr(5) }); - }, - closeTab: function(ev) + } + + closeTab = (ev) => { var self = this; var tab = ev.target.parentNode; @@ -59,4 +61,4 @@ var TabPanel = module.exports = React.createClass({ }, 200); ev.stopPropagation(); } -}); +} diff --git a/Util.js b/Util.js index cb2a130..2d17248 100644 --- a/Util.js +++ b/Util.js @@ -1,58 +1,61 @@ -var WeekDays = module.exports.WeekDays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]; -var Months = module.exports.Months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; - -module.exports.formatBytes = function(s) +export default class Util { - if (!s) return ''; - if (s < 1024) return s+' B'; - else if (s < 1024*1024) return (Math.round(s*10/1024)/10)+' KB'; - else if (s < 1024*1024*1024) return (Math.round(s*10/1024/1024)/10)+' MB'; - return (Math.round(s*10/1024/1024/1024)/10)+' GB'; -} + static WeekDays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]; + static Months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; -module.exports.formatDate = function(dt) -{ - if (!(dt instanceof Date)) - dt = new Date(dt.replace(' ', 'T')); - var tod = new Date(); - tod.setHours(0); - tod.setMinutes(0); - tod.setSeconds(0); - tod.setMilliseconds(0); - var prevweek = tod; - prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000; - if (dt.getTime() < prevweek) + static formatBytes(s) { - var d = dt.getDate(); - var m = dt.getMonth()+1; - return (d < 10 ? '0' : '')+d+'.'+(m < 10 ? '0' : '')+m+'.'+dt.getFullYear(); + if (!s) return ''; + if (s < 1024) return s+' B'; + else if (s < 1024*1024) return (Math.round(s*10/1024)/10)+' KB'; + else if (s < 1024*1024*1024) return (Math.round(s*10/1024/1024)/10)+' MB'; + return (Math.round(s*10/1024/1024/1024)/10)+' GB'; } - else if (dt.getTime() < tod.getTime()) - { - return WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Months[dt.getMonth()]; - } - var h = dt.getHours(); - var m = dt.getMinutes(); - return (h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m; -} -module.exports.getGroupName = function(k) -{ - if (k == 't') + static formatDate(dt) { - return 'Today'; + if (!(dt instanceof Date)) + dt = new Date(dt.replace(' ', 'T')); + var tod = new Date(); + tod.setHours(0); + tod.setMinutes(0); + tod.setSeconds(0); + tod.setMilliseconds(0); + var prevweek = tod; + prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000; + if (dt.getTime() < prevweek) + { + var d = dt.getDate(); + var m = dt.getMonth()+1; + return (d < 10 ? '0' : '')+d+'.'+(m < 10 ? '0' : '')+m+'.'+dt.getFullYear(); + } + else if (dt.getTime() < tod.getTime()) + { + return WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Months[dt.getMonth()]; + } + var h = dt.getHours(); + var m = dt.getMinutes(); + return (h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m; } - else if (k[0] == 'd') + + static getGroupName(k) { - return WeekDays[k.substr(1)%7]; + if (k == 't') + { + return 'Today'; + } + else if (k[0] == 'd') + { + return WeekDays[k.substr(1)%7]; + } + else if (k == 'pw') + { + return 'Last Week'; + } + else if (k[0] == 'm') + { + return Months[k.substr(1)-1]; + } + return k; } - else if (k == 'pw') - { - return 'Last Week'; - } - else if (k[0] == 'm') - { - return Months[k.substr(1)-1]; - } - return k; } diff --git a/mail.css b/mail.css index 27a85a7..431417c 100644 --- a/mail.css +++ b/mail.css @@ -17,6 +17,15 @@ html, body user-select: none; } +#app +{ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + .clear { clear: both; diff --git a/mail.js b/mail.js index bc2b7d0..db1c8fb 100644 --- a/mail.js +++ b/mail.js @@ -1,13 +1,13 @@ -const React = require('react'); -const ReactDOM = require('react-dom'); -const ComposeWindow = require('./ComposeWindow.js'); -const FolderList = require('./FolderList.js'); -const MessageList = require('./MessageList.js'); -const MessageView = require('./MessageView.js'); -const TabPanel = require('./TabPanel.js'); -const Store = require('./Store.js'); -const StoreListener = require('./StoreListener.js'); -const AllDropdowns = require('./AllDropdowns.js'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import ComposeWindow from './ComposeWindow.js'; +import FolderList from './FolderList.js'; +import MessageList from './MessageList.js'; +import MessageView from './MessageView.js'; +import TabPanel from './TabPanel.js'; +import Store from './Store.js'; +import StoreListener from './StoreListener.js'; +import AllDropdowns from './AllDropdowns.js'; window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame; @@ -19,13 +19,13 @@ var AllTabs = StoreListener(TabPanel, function(data) noclose: true, icon: 'mail_unread', title: 'Unread (64)', - children: [ MessageList, MessageView ] + children: [ , ] }, { icon: 'mail_drafts', i16: true, title: 'Compose Message', - children: [ ComposeWindow ] + children: [ ] } ] } }); @@ -34,9 +34,9 @@ ReactDOM.render(
{AllDropdowns()} - {AllTabs} +
, - document.body + document.getElementById('app') ); Store.loadAccounts(); diff --git a/mail.js.htm b/mail.js.htm index 9abdf39..2e9d1d4 100644 --- a/mail.js.htm +++ b/mail.js.htm @@ -5,13 +5,7 @@ - - +
diff --git a/package.json b/package.json index 93db64e..8b31147 100644 --- a/package.json +++ b/package.json @@ -6,33 +6,29 @@ "url": "http://yourcmc.ru/wiki/" }, "description": "LikeOperaMail", - "dependencies": { - }, + "dependencies": {}, "devDependencies": { - "browserify": "latest", - "babelify": "latest", - "watchify": "latest", - "babel-plugin-check-es2015-constants": "latest", - "babel-plugin-transform-es2015-arrow-functions": "latest", - "babel-plugin-transform-es2015-block-scoping": "latest", - "babel-plugin-transform-es2015-classes": "latest", - "babel-plugin-transform-es2015-computed-properties": "latest", - "babel-plugin-transform-es2015-for-of": "latest", - "babel-plugin-transform-es2015-destructuring": "latest", - "babel-plugin-transform-es2015-shorthand-properties": "latest", - "babel-plugin-transform-object-rest-spread": "latest", - "babel-plugin-transform-react-jsx": "latest", - "react": "latest", - "react-dom": "latest", - "uglifyjs": "latest", - "uglifyify": "latest", + "babel-core": "^6.26.0", + "babel-eslint": "^8.2.2", + "babel-loader": "^7.1.5", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.24.1", + "babel-preset-stage-1": "^6.24.1", "eslint": "latest", + "eslint-plugin-react": "^7.7.0", + "react": "^16.2.0", + "react-dom": "^16.2.0", "superagent": "latest", - "socket.io-client": "latest" + "socket.io-client": "latest", + "webpack": "^3.12.0", + "webpack-bundle-analyzer": "^2.13.1" }, "scripts": { - "compile": "browserify -t babelify -t uglifyify mail.js | uglifyjs -cm > mail.c.js", - "watch-dev": "watchify -t babelify mail.js -o mail.c.js", - "watch": "watchify -t babelify -t uglifyify mail.js -o 'uglifyjs -cm > mail.c.js'" + "lint": "eslint *.js", + "compile": "webpack --optimize-minimize", + "stats": "NODE_ENV=production webpack --optimize-minimize --profile --json > stats.json; webpack-bundle-analyzer stats.json -h 0.0.0.0", + "watch-dev": "NODE_ENV=development webpack -w", + "watch": "NODE_ENV=production webpack -w --optimize-minimize" } } diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..49dbaa3 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const path = require('path'); + +module.exports = { + entry: { + main: [ "babel-polyfill", './mail.js' ] + }, + context: __dirname, + output: { + path: __dirname, + filename: './mail.c.js' + }, + devtool: 'cheap-module-source-map', + module: { + rules: [ + { + test: /.jsx?$/, + loader: 'babel-loader', + exclude: /node_modules/ + }, + { + test: /\.css$/, + use: [ + { + loader: "style-loader", + options: { + singleton: true + } + }, + { + loader: "css-loader", + options: { + modules: true, // default is false + sourceMap: true, + importLoaders: 1, + localIdentName: "[name]--[local]--[hash:base64:8]" + } + } + ] + } + ] + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: JSON.stringify(process.env.NODE_ENV || "production") + } + }) + ], + performance: { + maxEntrypointSize: 5000000, + maxAssetSize: 5000000 + } +};