├── .gitignore
├── README.md
├── component.json
├── fis-conf.js
├── index.html
├── modules
├── actions
│ └── todos.ts
├── components
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── MainSection.tsx
│ ├── TodoItem.tsx
│ └── TodoTextInput.tsx
├── constants
│ ├── ActionTypes.ts
│ └── TodoFilters.ts
├── containers
│ └── App.tsx
├── index.tsx
├── reducers
│ ├── index.ts
│ └── todos.ts
└── store
│ └── configureStore.ts
├── package.json
└── static
├── index.css
└── mod.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | output
4 |
5 | /components
6 |
7 | .DS_Store
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FIS3 React + Redux 的 todo demo.
2 |
3 | ## 安装 fis3
4 |
5 | `npm install fis3 -g`
6 |
7 | **注意 fis3 版本请用 3.2.9 或者以上,否则需要把 .ts 文件后缀假如文本类型列表中**
8 |
9 | ## 初始化
10 |
11 | ```bash
12 | mkdir demo
13 | cd demo
14 | fis3 init react-redux-todo
15 | ```
16 |
17 | ## 运行 & 预览
18 |
19 | ```bash
20 | fis3 release
21 | fis3 server start
22 | ```
23 |
24 | 或者
25 |
26 | ```
27 | npm start
28 | ```
29 |
30 | ## 产出产品代码
31 |
32 | 只启用了 js 压缩和 合并。
33 |
34 | ```
35 | fis3 release production -d ./output
36 | ```
37 |
--------------------------------------------------------------------------------
/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": [
3 | "react@^0.14.2",
4 | "react-dom@^0.14.2",
5 | "redux@^3.0.4",
6 | "react-redux@^4.0.0",
7 | "classnames"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/fis-conf.js:
--------------------------------------------------------------------------------
1 | fis.set('project.files', '/index.html'); // 按需编译。
2 |
3 | // 采用 commonjs 模块化方案。
4 | fis.hook('commonjs', {
5 | baseUrl: './modules',
6 | extList: ['.js', '.ts', '.tsx']
7 | });
8 |
9 | fis.match('*.{ts,tsx}', {
10 | parser: fis.plugin('typescript'),
11 | rExt: '.js'
12 | });
13 |
14 | // 设置成是模块化 js
15 | fis.match('/{components,modules}/**.{js,ts,tsx}', {
16 | isMod: true
17 | });
18 |
19 | fis.match('::package', {
20 | // 本项目为纯前段项目,所以用 loader 编译器加载,
21 | // 如果用后端运行时框架,请不要使用。
22 | postpackager: fis.plugin('loader', {
23 | useInlineMap: true
24 | })
25 | });
26 |
27 |
28 | // 请用 fis3 release production 来启用。
29 | fis.media('production')
30 |
31 | // 对 js 做 uglify 压缩。
32 | .match('*.{js,ts,tsx}', {
33 | optimizer: fis.plugin('uglify-js')
34 | })
35 |
36 | .match('::package', {
37 |
38 | // 更多用法请参考: https://github.com/fex-team/fis3-packager-deps-pack
39 | packager: fis.plugin('deps-pack', {
40 | 'pkg/index.js': /*当有多条时,请用数组*/[
41 | 'modules/index.tsx',
42 | 'modules/index.tsx:deps', // 以及其所有依赖
43 | ]
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux TodoMVC example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/modules/actions/todos.ts:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 |
3 | export function addTodo(text) {
4 | return { type: types.ADD_TODO, text }
5 | }
6 |
7 | export function deleteTodo(id) {
8 | return { type: types.DELETE_TODO, id }
9 | }
10 |
11 | export function editTodo(id, text) {
12 | return { type: types.EDIT_TODO, id, text }
13 | }
14 |
15 | export function completeTodo(id) {
16 | return { type: types.COMPLETE_TODO, id }
17 | }
18 |
19 | export function completeAll() {
20 | return { type: types.COMPLETE_ALL }
21 | }
22 |
23 | export function clearCompleted() {
24 | return { type: types.CLEAR_COMPLETED }
25 | }
26 |
--------------------------------------------------------------------------------
/modules/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import classnames from 'classnames'
3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
4 |
5 | const FILTER_TITLES = {
6 | [SHOW_ALL]: 'All',
7 | [SHOW_ACTIVE]: 'Active',
8 | [SHOW_COMPLETED]: 'Completed'
9 | }
10 |
11 | class Footer extends Component {
12 | renderTodoCount() {
13 | const { activeCount } = this.props
14 | const itemWord = activeCount === 1 ? 'item' : 'items'
15 |
16 | return (
17 |
18 | {activeCount || 'No'} {itemWord} left
19 |
20 | )
21 | }
22 |
23 | renderFilterLink(filter) {
24 | const title = FILTER_TITLES[filter]
25 | const { filter: selectedFilter, onShow } = this.props
26 |
27 | return (
28 | onShow(filter)}>
31 | {title}
32 |
33 | )
34 | }
35 |
36 | renderClearButton() {
37 | const { completedCount, onClearCompleted } = this.props
38 | if (completedCount > 0) {
39 | return (
40 |
44 | )
45 | }
46 | }
47 |
48 | render() {
49 | return (
50 |
61 | )
62 | }
63 | }
64 |
65 | Footer.propTypes = {
66 | completedCount: PropTypes.number.isRequired,
67 | activeCount: PropTypes.number.isRequired,
68 | filter: PropTypes.string.isRequired,
69 | onClearCompleted: PropTypes.func.isRequired,
70 | onShow: PropTypes.func.isRequired
71 | }
72 |
73 | export default Footer
74 |
--------------------------------------------------------------------------------
/modules/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import TodoTextInput from './TodoTextInput'
3 |
4 | class Header extends Component {
5 | handleSave(text) {
6 | if (text.length !== 0) {
7 | this.props.addTodo(text)
8 | }
9 | }
10 |
11 | render() {
12 | return (
13 |
19 | )
20 | }
21 | }
22 |
23 | Header.propTypes = {
24 | addTodo: PropTypes.func.isRequired
25 | }
26 |
27 | export default Header
28 |
--------------------------------------------------------------------------------
/modules/components/MainSection.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import TodoItem from './TodoItem'
3 | import Footer from './Footer'
4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'
5 |
6 | const TODO_FILTERS = {
7 | [SHOW_ALL]: () => true,
8 | [SHOW_ACTIVE]: todo => !todo.completed,
9 | [SHOW_COMPLETED]: todo => todo.completed
10 | }
11 |
12 | class MainSection extends Component {
13 | constructor(props, context) {
14 | super(props, context)
15 | this.state = { filter: SHOW_ALL }
16 | }
17 |
18 | handleClearCompleted() {
19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed)
20 | if (atLeastOneCompleted) {
21 | this.props.actions.clearCompleted()
22 | }
23 | }
24 |
25 | handleShow(filter) {
26 | this.setState({ filter })
27 | }
28 |
29 | renderToggleAll(completedCount) {
30 | const { todos, actions } = this.props
31 | if (todos.length > 0) {
32 | return (
33 |
37 | )
38 | }
39 | }
40 |
41 | renderFooter(completedCount) {
42 | const { todos } = this.props
43 | const { filter } = this.state
44 | const activeCount = todos.length - completedCount
45 |
46 | if (todos.length) {
47 | return (
48 |
53 | )
54 | }
55 | }
56 |
57 | render() {
58 | const { todos, actions } = this.props
59 | const { filter } = this.state
60 |
61 | const filteredTodos = todos.filter(TODO_FILTERS[filter])
62 | const completedCount = todos.reduce((count, todo) =>
63 | todo.completed ? count + 1 : count,
64 | 0
65 | )
66 |
67 | return (
68 |
69 | {this.renderToggleAll(completedCount) }
70 |
71 | {filteredTodos.map(todo =>
72 |
73 | ) }
74 |
75 | {this.renderFooter(completedCount) }
76 |
77 | )
78 | }
79 | }
80 |
81 | MainSection.propTypes = {
82 | todos: PropTypes.array.isRequired,
83 | actions: PropTypes.object.isRequired
84 | }
85 |
86 | export default MainSection
87 |
--------------------------------------------------------------------------------
/modules/components/TodoItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import TodoTextInput from './TodoTextInput'
4 |
5 | class TodoItem extends Component {
6 | constructor(props, context) {
7 | super(props, context)
8 | this.state = {
9 | editing: false
10 | }
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true })
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id)
20 | } else {
21 | this.props.editTodo(id, text)
22 | }
23 | this.setState({ editing: false })
24 | }
25 |
26 | render() {
27 | const { todo, completeTodo, deleteTodo } = this.props
28 |
29 | let element
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo.id, text)} />
35 | )
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
46 |
49 | )
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | )
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | }
69 |
70 | export default TodoItem
71 |
--------------------------------------------------------------------------------
/modules/components/TodoTextInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context)
7 | this.state = {
8 | text: this.props.text || ''
9 | }
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim()
14 | if (e.which === 13) {
15 | this.props.onSave(text)
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' })
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value })
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value)
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | )
47 | }
48 | }
49 |
50 | TodoTextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | }
57 |
58 | export default TodoTextInput
59 |
--------------------------------------------------------------------------------
/modules/constants/ActionTypes.ts:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DELETE_TODO = 'DELETE_TODO'
3 | export const EDIT_TODO = 'EDIT_TODO'
4 | export const COMPLETE_TODO = 'COMPLETE_TODO'
5 | export const COMPLETE_ALL = 'COMPLETE_ALL'
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
7 |
--------------------------------------------------------------------------------
/modules/constants/TodoFilters.ts:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all'
2 | export const SHOW_COMPLETED = 'show_completed'
3 | export const SHOW_ACTIVE = 'show_active'
4 |
--------------------------------------------------------------------------------
/modules/containers/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { bindActionCreators } from 'redux'
3 | import { connect } from 'react-redux'
4 | import Header from '../components/Header'
5 | import MainSection from '../components/MainSection'
6 | import * as TodoActions from '../actions/todos'
7 |
8 | class App extends Component {
9 | render() {
10 | const { todos, actions } = this.props
11 | return (
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | App.propTypes = {
21 | todos: PropTypes.array.isRequired,
22 | actions: PropTypes.object.isRequired
23 | }
24 |
25 | function mapStateToProps(state) {
26 | return {
27 | todos: state.todos
28 | }
29 | }
30 |
31 | function mapDispatchToProps(dispatch) {
32 | return {
33 | actions: bindActionCreators(TodoActions, dispatch)
34 | }
35 | }
36 |
37 | export default connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | )(App)
41 |
--------------------------------------------------------------------------------
/modules/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import App from './containers/App'
5 | import configureStore from './store/configureStore'
6 |
7 | const store = configureStore()
8 |
9 | render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | )
15 |
--------------------------------------------------------------------------------
/modules/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 |
4 | const rootReducer = combineReducers({
5 | todos
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/modules/reducers/todos.ts:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'
2 |
3 | const initialState = [
4 | {
5 | text: 'Use Redux',
6 | completed: false,
7 | id: 0
8 | }
9 | ]
10 |
11 | export default function todos(state = initialState, action) {
12 | switch (action.type) {
13 | case ADD_TODO:
14 | return [
15 | {
16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
17 | completed: false,
18 | text: action.text
19 | },
20 | ...state
21 | ]
22 |
23 | case DELETE_TODO:
24 | return state.filter(todo =>
25 | todo.id !== action.id
26 | )
27 |
28 | case EDIT_TODO:
29 | return state.map(todo =>
30 | todo.id === action.id ?
31 | Object.assign({}, todo, { text: action.text }) :
32 | todo
33 | )
34 |
35 | case COMPLETE_TODO:
36 | return state.map(todo =>
37 | todo.id === action.id ?
38 | Object.assign({}, todo, { completed: !todo.completed }) :
39 | todo
40 | )
41 |
42 | case COMPLETE_ALL:
43 | const areAllMarked = state.every(todo => todo.completed)
44 | return state.map(todo => Object.assign({}, todo, {
45 | completed: !areAllMarked
46 | }))
47 |
48 | case CLEAR_COMPLETED:
49 | return state.filter(todo => todo.completed === false)
50 |
51 | default:
52 | return state
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/modules/store/configureStore.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux'
2 | import rootReducer from '../reducers'
3 |
4 | export default function configureStore(initialState) {
5 | const store = createStore(rootReducer, initialState)
6 |
7 | if (module.hot) {
8 | // Enable Webpack hot module replacement for reducers
9 | module.hot.accept('../reducers', () => {
10 | const nextReducer = require('../reducers')
11 | store.replaceReducer(nextReducer)
12 | })
13 | }
14 |
15 | return store
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-todo",
3 | "version": "0.0.0",
4 | "description": "react + redux 的 todo demo. 供学习参考。",
5 | "dependencies": {
6 | "fis3-hook-commonjs": "^0.1.3",
7 | "fis3-postpackager-loader": "^1.3.6",
8 | "fis3-parser-typescript": "^0.1.4",
9 | "fis3-packager-deps-pack": "^0.0.1"
10 | },
11 | "devDependencies": {},
12 | "scripts": {
13 | "test": "echo \"Error: no test specified\" && exit 1",
14 | "start": "fis3 release -cd ./output && fis3 server start --www output --no-daemon",
15 | "dev": "rm -rf output && fis3 release -cwd output"
16 | },
17 | "keywords": [
18 | "esj"
19 | ],
20 | "author": "who care",
21 | "license": "BSD",
22 | "repository": "git@gitlab.freely.io:esj/esj-weixin.git"
23 | }
24 |
--------------------------------------------------------------------------------
/static/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-font-smoothing: antialiased;
21 | font-smoothing: antialiased;
22 | }
23 |
24 | body {
25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
26 | line-height: 1.4em;
27 | background: #f5f5f5;
28 | color: #4d4d4d;
29 | min-width: 230px;
30 | max-width: 550px;
31 | margin: 0 auto;
32 | -webkit-font-smoothing: antialiased;
33 | -moz-font-smoothing: antialiased;
34 | font-smoothing: antialiased;
35 | font-weight: 300;
36 | }
37 |
38 | button,
39 | input[type="checkbox"] {
40 | outline: none;
41 | }
42 |
43 | .hidden {
44 | display: none;
45 | }
46 |
47 | .todoapp {
48 | background: #fff;
49 | margin: 130px 0 40px 0;
50 | position: relative;
51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
53 | }
54 |
55 | .todoapp input::-webkit-input-placeholder {
56 | font-style: italic;
57 | font-weight: 300;
58 | color: #e6e6e6;
59 | }
60 |
61 | .todoapp input::-moz-placeholder {
62 | font-style: italic;
63 | font-weight: 300;
64 | color: #e6e6e6;
65 | }
66 |
67 | .todoapp input::input-placeholder {
68 | font-style: italic;
69 | font-weight: 300;
70 | color: #e6e6e6;
71 | }
72 |
73 | .todoapp h1 {
74 | position: absolute;
75 | top: -155px;
76 | width: 100%;
77 | font-size: 100px;
78 | font-weight: 100;
79 | text-align: center;
80 | color: rgba(175, 47, 47, 0.15);
81 | -webkit-text-rendering: optimizeLegibility;
82 | -moz-text-rendering: optimizeLegibility;
83 | text-rendering: optimizeLegibility;
84 | }
85 |
86 | .new-todo,
87 | .edit {
88 | position: relative;
89 | margin: 0;
90 | width: 100%;
91 | font-size: 24px;
92 | font-family: inherit;
93 | font-weight: inherit;
94 | line-height: 1.4em;
95 | border: 0;
96 | outline: none;
97 | color: inherit;
98 | padding: 6px;
99 | border: 1px solid #999;
100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
101 | box-sizing: border-box;
102 | -webkit-font-smoothing: antialiased;
103 | -moz-font-smoothing: antialiased;
104 | font-smoothing: antialiased;
105 | }
106 |
107 | .new-todo {
108 | padding: 16px 16px 16px 60px;
109 | border: none;
110 | background: rgba(0, 0, 0, 0.003);
111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
112 | }
113 |
114 | .main {
115 | position: relative;
116 | z-index: 2;
117 | border-top: 1px solid #e6e6e6;
118 | }
119 |
120 | label[for='toggle-all'] {
121 | display: none;
122 | }
123 |
124 | .toggle-all {
125 | position: absolute;
126 | top: -55px;
127 | left: -12px;
128 | width: 60px;
129 | height: 34px;
130 | text-align: center;
131 | border: none; /* Mobile Safari */
132 | }
133 |
134 | .toggle-all:before {
135 | content: '❯';
136 | font-size: 22px;
137 | color: #e6e6e6;
138 | padding: 10px 27px 10px 27px;
139 | }
140 |
141 | .toggle-all:checked:before {
142 | color: #737373;
143 | }
144 |
145 | .todo-list {
146 | margin: 0;
147 | padding: 0;
148 | list-style: none;
149 | }
150 |
151 | .todo-list li {
152 | position: relative;
153 | font-size: 24px;
154 | border-bottom: 1px solid #ededed;
155 | }
156 |
157 | .todo-list li:last-child {
158 | border-bottom: none;
159 | }
160 |
161 | .todo-list li.editing {
162 | border-bottom: none;
163 | padding: 0;
164 | }
165 |
166 | .todo-list li.editing .edit {
167 | display: block;
168 | width: 506px;
169 | padding: 13px 17px 12px 17px;
170 | margin: 0 0 0 43px;
171 | }
172 |
173 | .todo-list li.editing .view {
174 | display: none;
175 | }
176 |
177 | .todo-list li .toggle {
178 | text-align: center;
179 | width: 40px;
180 | /* auto, since non-WebKit browsers doesn't support input styling */
181 | height: auto;
182 | position: absolute;
183 | top: 0;
184 | bottom: 0;
185 | margin: auto 0;
186 | border: none; /* Mobile Safari */
187 | -webkit-appearance: none;
188 | appearance: none;
189 | }
190 |
191 | .todo-list li .toggle:after {
192 | content: url('data:image/svg+xml;utf8,');
193 | }
194 |
195 | .todo-list li .toggle:checked:after {
196 | content: url('data:image/svg+xml;utf8,');
197 | }
198 |
199 | .todo-list li label {
200 | white-space: pre-line;
201 | word-break: break-all;
202 | padding: 15px 60px 15px 15px;
203 | margin-left: 45px;
204 | display: block;
205 | line-height: 1.2;
206 | transition: color 0.4s;
207 | }
208 |
209 | .todo-list li.completed label {
210 | color: #d9d9d9;
211 | text-decoration: line-through;
212 | }
213 |
214 | .todo-list li .destroy {
215 | display: none;
216 | position: absolute;
217 | top: 0;
218 | right: 10px;
219 | bottom: 0;
220 | width: 40px;
221 | height: 40px;
222 | margin: auto 0;
223 | font-size: 30px;
224 | color: #cc9a9a;
225 | margin-bottom: 11px;
226 | transition: color 0.2s ease-out;
227 | }
228 |
229 | .todo-list li .destroy:hover {
230 | color: #af5b5e;
231 | }
232 |
233 | .todo-list li .destroy:after {
234 | content: '×';
235 | }
236 |
237 | .todo-list li:hover .destroy {
238 | display: block;
239 | }
240 |
241 | .todo-list li .edit {
242 | display: none;
243 | }
244 |
245 | .todo-list li.editing:last-child {
246 | margin-bottom: -1px;
247 | }
248 |
249 | .footer {
250 | color: #777;
251 | padding: 10px 15px;
252 | height: 20px;
253 | text-align: center;
254 | border-top: 1px solid #e6e6e6;
255 | }
256 |
257 | .footer:before {
258 | content: '';
259 | position: absolute;
260 | right: 0;
261 | bottom: 0;
262 | left: 0;
263 | height: 50px;
264 | overflow: hidden;
265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
266 | 0 8px 0 -3px #f6f6f6,
267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
268 | 0 16px 0 -6px #f6f6f6,
269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
270 | }
271 |
272 | .todo-count {
273 | float: left;
274 | text-align: left;
275 | }
276 |
277 | .todo-count strong {
278 | font-weight: 300;
279 | }
280 |
281 | .filters {
282 | margin: 0;
283 | padding: 0;
284 | list-style: none;
285 | position: absolute;
286 | right: 0;
287 | left: 0;
288 | }
289 |
290 | .filters li {
291 | display: inline;
292 | }
293 |
294 | .filters li a {
295 | color: inherit;
296 | margin: 3px;
297 | padding: 3px 7px;
298 | text-decoration: none;
299 | border: 1px solid transparent;
300 | border-radius: 3px;
301 | }
302 |
303 | .filters li a.selected,
304 | .filters li a:hover {
305 | border-color: rgba(175, 47, 47, 0.1);
306 | }
307 |
308 | .filters li a.selected {
309 | border-color: rgba(175, 47, 47, 0.2);
310 | }
311 |
312 | .clear-completed,
313 | html .clear-completed:active {
314 | float: right;
315 | position: relative;
316 | line-height: 20px;
317 | text-decoration: none;
318 | cursor: pointer;
319 | }
320 |
321 | .clear-completed:hover {
322 | text-decoration: underline;
323 | }
324 |
325 | .info {
326 | margin: 65px auto 0;
327 | color: #bfbfbf;
328 | font-size: 10px;
329 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
330 | text-align: center;
331 | }
332 |
333 | .info p {
334 | line-height: 1;
335 | }
336 |
337 | .info a {
338 | color: inherit;
339 | text-decoration: none;
340 | font-weight: 400;
341 | }
342 |
343 | .info a:hover {
344 | text-decoration: underline;
345 | }
346 |
347 | /*
348 | Hack to remove background from Mobile Safari.
349 | Can't use it globally since it destroys checkboxes in Firefox
350 | */
351 | @media screen and (-webkit-min-device-pixel-ratio:0) {
352 | .toggle-all,
353 | .todo-list li .toggle {
354 | background: none;
355 | }
356 |
357 | .todo-list li .toggle {
358 | height: 40px;
359 | }
360 |
361 | .toggle-all {
362 | -webkit-transform: rotate(90deg);
363 | transform: rotate(90deg);
364 | -webkit-appearance: none;
365 | appearance: none;
366 | }
367 | }
368 |
369 | @media (max-width: 430px) {
370 | .footer {
371 | height: 50px;
372 | }
373 |
374 | .filters {
375 | bottom: 10px;
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/static/mod.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file: mod.js
3 | * @author fis
4 | * ver: 1.0.11
5 | * update: 2015/05/14
6 | * https://github.com/fex-team/mod
7 | */
8 | var require;
9 |
10 | /* eslint-disable no-unused-vars */
11 | var define;
12 |
13 | (function (global) {
14 |
15 | // 避免重复加载而导致已定义模块丢失
16 | if (require) {
17 | return;
18 | }
19 |
20 | var head = document.getElementsByTagName('head')[0];
21 | var loadingMap = {};
22 | var factoryMap = {};
23 | var modulesMap = {};
24 | var scriptsMap = {};
25 | var resMap = {};
26 | var pkgMap = {};
27 |
28 | var createScript = function (url, onerror) {
29 | if (url in scriptsMap) {
30 | return;
31 | }
32 |
33 | scriptsMap[url] = true;
34 |
35 | var script = document.createElement('script');
36 | if (onerror) {
37 | var tid = setTimeout(onerror, require.timeout);
38 |
39 | script.onerror = function () {
40 | clearTimeout(tid);
41 | onerror();
42 | };
43 |
44 | var onload = function () {
45 | clearTimeout(tid);
46 | };
47 |
48 | if ('onload' in script) {
49 | script.onload = onload;
50 | }
51 | else {
52 | script.onreadystatechange = function () {
53 | if (this.readyState === 'loaded' || this.readyState === 'complete') {
54 | onload();
55 | }
56 | };
57 | }
58 | }
59 | script.type = 'text/javascript';
60 | script.src = url;
61 | head.appendChild(script);
62 | head.removeChild(script);
63 | return script;
64 | };
65 |
66 | var loadScript = function (id, callback, onerror) {
67 | var queue = loadingMap[id] || (loadingMap[id] = []);
68 | queue.push(callback);
69 |
70 | //
71 | // resource map query
72 | //
73 | var res = resMap[id] || resMap[id + '.js'] || {};
74 | var pkg = res.pkg;
75 | var url;
76 |
77 | if (pkg) {
78 | url = pkgMap[pkg].url;
79 | }
80 | else {
81 | url = res.url || id;
82 | }
83 |
84 | createScript(url, onerror && function () {
85 | onerror(id);
86 | });
87 | };
88 |
89 | define = function (id, factory) {
90 | id = id.replace(/\.js$/i, '');
91 | factoryMap[id] = factory;
92 |
93 | var queue = loadingMap[id];
94 | if (queue) {
95 | for (var i = 0, n = queue.length; i < n; i++) {
96 | queue[i]();
97 | }
98 | delete loadingMap[id];
99 | }
100 | };
101 |
102 | require = function (id) {
103 |
104 | // compatible with require([dep, dep2...]) syntax.
105 | if (id && id.splice) {
106 | return require.async.apply(this, arguments);
107 | }
108 |
109 | id = require.alias(id);
110 |
111 | var mod = modulesMap[id];
112 | if (mod) {
113 | return mod.exports;
114 | }
115 |
116 | //
117 | // init module
118 | //
119 | var factory = factoryMap[id];
120 | if (!factory) {
121 | throw '[ModJS] Cannot find module `' + id + '`';
122 | }
123 |
124 | mod = modulesMap[id] = {
125 | exports: {}
126 | };
127 |
128 | //
129 | // factory: function OR value
130 | //
131 | var ret = (typeof factory === 'function') ? factory.apply(mod, [require, mod.exports, mod]) : factory;
132 |
133 | if (ret) {
134 | mod.exports = ret;
135 | }
136 |
137 | // es6 module support
138 | if (mod.exports && !mod.exports.default) {
139 | mod.exports.default = mod.exports;
140 | }
141 |
142 | return mod.exports;
143 | };
144 |
145 | require.async = function (names, onload, onerror) {
146 | if (typeof names === 'string') {
147 | names = [names];
148 | }
149 |
150 | var needMap = {};
151 | var needNum = 0;
152 |
153 | function findNeed(depArr) {
154 | var child;
155 |
156 | for (var i = 0, n = depArr.length; i < n; i++) {
157 | //
158 | // skip loading or loaded
159 | //
160 | var dep = require.alias(depArr[i]);
161 |
162 | if (dep in factoryMap) {
163 | // check whether loaded resource's deps is loaded or not
164 | child = resMap[dep] || resMap[dep + '.js'];
165 | if (child && 'deps' in child) {
166 | findNeed(child.deps);
167 | }
168 | continue;
169 | }
170 |
171 | if (dep in needMap) {
172 | continue;
173 | }
174 |
175 | needMap[dep] = true;
176 | needNum++;
177 | loadScript(dep, updateNeed, onerror);
178 |
179 | child = resMap[dep] || resMap[dep + '.js'];
180 | if (child && 'deps' in child) {
181 | findNeed(child.deps);
182 | }
183 | }
184 | }
185 |
186 | function updateNeed() {
187 | if (0 === needNum--) {
188 | var args = [];
189 | for (var i = 0, n = names.length; i < n; i++) {
190 | args[i] = require(names[i]);
191 | }
192 |
193 | onload && onload.apply(global, args);
194 | }
195 | }
196 |
197 | findNeed(names);
198 | updateNeed();
199 | };
200 |
201 | require.resourceMap = function (obj) {
202 | var k;
203 | var col;
204 |
205 | // merge `res` & `pkg` fields
206 | col = obj.res;
207 | for (k in col) {
208 | if (col.hasOwnProperty(k)) {
209 | resMap[k] = col[k];
210 | }
211 | }
212 |
213 | col = obj.pkg;
214 | for (k in col) {
215 | if (col.hasOwnProperty(k)) {
216 | pkgMap[k] = col[k];
217 | }
218 | }
219 | };
220 |
221 | require.loadJs = function (url) {
222 | createScript(url);
223 | };
224 |
225 | /*require.loadCss = function (cfg) {
226 | if (cfg.content) {
227 | var sty = document.createElement('style');
228 | sty.type = 'text/css';
229 |
230 | if (sty.styleSheet) { // IE
231 | sty.styleSheet.cssText = cfg.content;
232 | }
233 | else {
234 | sty.innerHTML = cfg.content;
235 | }
236 | head.appendChild(sty);
237 | }
238 | else if (cfg.url) {
239 | var link = document.createElement('link');
240 | link.href = cfg.url;
241 | link.rel = 'stylesheet';
242 | link.type = 'text/css';
243 | head.appendChild(link);
244 | }
245 | };*/
246 |
247 |
248 | require.alias = function (id) {
249 | return id.replace(/\.js$/i, '');
250 | };
251 |
252 | require.timeout = 5000;
253 |
254 | })(this);
255 |
--------------------------------------------------------------------------------