Initial commit
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["env", {
|
||||||
|
"modules": false,
|
||||||
|
"targets": {
|
||||||
|
"browsers": ["iOS >= 8", "Android >= 4"]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
"plugins": ["transform-object-rest-spread"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"presets": [["env", {
|
||||||
|
"modules": false
|
||||||
|
}], "stage-2"],
|
||||||
|
"plugins": ["istanbul"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
|
@ -0,0 +1,11 @@
|
||||||
|
build/*.js
|
||||||
|
config/*.js
|
||||||
|
lib/*
|
||||||
|
output/*
|
||||||
|
examples/*
|
||||||
|
site/*
|
||||||
|
**/*.spec.*
|
||||||
|
**/demo/data/**
|
||||||
|
scroller.js
|
||||||
|
animate.js
|
||||||
|
gulpfile.js
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {ecmaVersion: 8, sourceType: 'module', ecmaFeatures: {jsx: true, experimentalObjectRestSpread: true}},
|
||||||
|
env: {es6: true, node: true, browser: true},
|
||||||
|
plugins: ['html', 'json'],
|
||||||
|
extends: ['eslint-config-aesir-mandatory'],
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
lib/
|
||||||
|
output/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
test/unit/coverage
|
||||||
|
test/e2e/reports
|
||||||
|
selenium-debug.log
|
||||||
|
site/public
|
||||||
|
site/dist
|
||||||
|
site/build/bin/algolia-key.js
|
||||||
|
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
|
@ -0,0 +1,80 @@
|
||||||
|
---
|
||||||
|
title: 更新日志
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- CUTOFF -->
|
||||||
|
### 0.4.13
|
||||||
|
|
||||||
|
`2018-01-19`
|
||||||
|
|
||||||
|
- **Feature**
|
||||||
|
- `ActionBar`属性`has-text`默认值为是否存在`slot`,即如果使用插槽可忽略`has-text`
|
||||||
|
|
||||||
|
- **Fix**
|
||||||
|
- 修复`DatePicker`月份`undefined` #26
|
||||||
|
- 修复`Icon`部分安卓机无法展示内置SVG问题 #27
|
||||||
|
- 修复`ActionBar`的`Props`默认值设置无效 #28
|
||||||
|
- 修复`TabPicker`在安卓`6.1`异步级联滑动问题
|
||||||
|
|
||||||
|
|
||||||
|
### 0.4.8
|
||||||
|
|
||||||
|
`2018-01-16`
|
||||||
|
|
||||||
|
- **Feature**
|
||||||
|
|
||||||
|
- 新增组件`Codebox`, `Cashier`, `Chart`
|
||||||
|
- `FieldItem`的属性`customized`默认值为是否存在`slot`,即如果使用插槽可忽略`customized` #23
|
||||||
|
- `InputItem`新增属性`is-title-latent`用于支持表单标题延迟显示
|
||||||
|
- `Radio`的`v-model`绑定由`options: Array<{text, value}>`中的`text`修改为`value`
|
||||||
|
- `NumberKeyboard`新增属性`type`和`is-view`用于支持不同主题和键盘页面内嵌展示
|
||||||
|
|
||||||
|
- **Fix**
|
||||||
|
- 修复`PopupTitleBar`无法引入
|
||||||
|
- 修复`ImageViewer`部分安卓机无法关闭问题 #20
|
||||||
|
- 修复`Picker`的`refresh`方法导致列表滚动异常 #24
|
||||||
|
- 修复`InputItem`输入汉字异常 #25
|
||||||
|
|
||||||
|
|
||||||
|
<!-- CUTOFF -->
|
||||||
|
### 0.3.0
|
||||||
|
|
||||||
|
`2017-12-18`
|
||||||
|
|
||||||
|
- **Feature**
|
||||||
|
|
||||||
|
- `Radio`, `Selector`, `DropMenu`, `Tabs`, `TabPicker`支持`slot-scope`
|
||||||
|
- `TabPicker`新增`data-struct`,`asyncFunc`支持普通,级联和异步三种数据结构
|
||||||
|
- `Tip`新增`show/hide`事件
|
||||||
|
- `Picker`新增`initial`事件
|
||||||
|
- `DatePicker`新增`text-render`钩子方法满足列项内容自定义
|
||||||
|
|
||||||
|
- **Fix**
|
||||||
|
- 修复`SwiperItem`无法引入错误
|
||||||
|
- 修复`InputItem`, `NumberKeyboard`双向绑定异常
|
||||||
|
- 修复`Popup`动画监听异常导致`hide`事件可能不会触发
|
||||||
|
- 修复`Dialog`和`Toast`被遮盖问题
|
||||||
|
- 修复`FieldItem`, `Tag`样式问题
|
||||||
|
|
||||||
|
### 0.2.0
|
||||||
|
|
||||||
|
`2017-11-28`
|
||||||
|
|
||||||
|
- **Feature**
|
||||||
|
|
||||||
|
- 新增组件`Radio`, `DatePicker`, `Captcha`
|
||||||
|
- `Field`新增`solid`属性用来固定布局
|
||||||
|
- `Steps`新增配置`icon`属性
|
||||||
|
- `Agree`新增`slot`用来展示文案
|
||||||
|
|
||||||
|
- **Fix**
|
||||||
|
- 修复部分文档,样式和错误
|
||||||
|
|
||||||
|
<!-- CUTOFF -->
|
||||||
|
### 0.1.0
|
||||||
|
|
||||||
|
`2017-11-21`
|
||||||
|
|
||||||
|
- **Feature**
|
||||||
|
|
||||||
|
- 完成开发版开发,用于内部体验和测试
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Contribution Guideline
|
||||||
|
|
||||||
|
Thanks for considering to contribute this project. All issues and pull requests are highly appreciated.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
Before sending pull request to this project, please read and follow guidelines below.
|
||||||
|
|
||||||
|
1. Branch: We only accept pull request on `dev` branch.
|
||||||
|
2. Coding style: Follow the coding style used in mand-mobile.
|
||||||
|
3. Commit message: Use English and be aware of your spelling.
|
||||||
|
4. Test: Make sure to test your code.
|
||||||
|
|
||||||
|
Add device mode, API version, related log, screenshots and other related information in your pull request if possible.
|
||||||
|
|
||||||
|
NOTE: We assume all your contribution can be licensed under the [Apache License 2.0](https://github.com/didi/mand-mobile/blob/master/LICENSE).
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
We love clearly described issues. :)
|
||||||
|
|
||||||
|
Following information can help us to resolve issues faster.
|
||||||
|
|
||||||
|
* Device mode and hardware information.
|
||||||
|
* API version.
|
||||||
|
* Logs.
|
||||||
|
* Screenshots.
|
||||||
|
* Steps to reproduce the issue.
|
|
@ -0,0 +1,433 @@
|
||||||
|
Apache License
|
||||||
|
|
||||||
|
Version 2.0, January 2004
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (C) 2017 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,141 @@
|
||||||
|
<div align="center">
|
||||||
|
<a href="#">
|
||||||
|
<img width="80" src="./assets/logo.png" alt="LOGO">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div align="center">
|
||||||
|
<a href="http://forthebadge.com">
|
||||||
|
<img src="http://forthebadge.com/images/badges/made-with-vue.svg">
|
||||||
|
</a>
|
||||||
|
<a href="http://forthebadge.com">
|
||||||
|
<img src="http://forthebadge.com/images/badges/built-with-love.svg">
|
||||||
|
</a>
|
||||||
|
<a href="http://forthebadge.com">
|
||||||
|
<img src="http://forthebadge.com/images/badges/makes-people-smile.svg">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# mand-mobile
|
||||||
|
|
||||||
|
[](https://travis-ci.org/adidi/mand-mobile)
|
||||||
|
[](https://codecov.io/gh/didi/mand-mobile/branch/master)
|
||||||
|
[](https://www.npmjs.org/package/mand-mobile)
|
||||||
|
[](http://npmtrends.com/mand-mobile)
|
||||||
|
[](http://isitmaintained.com/project/didi/mand-mobile "Average time to resolve an issue")
|
||||||
|
[](http://isitmaintained.com/project/didi/mand-mobile "Percentage of issues still open")
|
||||||
|
|
||||||
|
A mobile UI toolkit, based on `Vue.js 2`, designed for financial scenes.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
* [Home](https://didi.github.io/mand-mobile/)
|
||||||
|
* [Developer Instruction](site/docs/development.md)
|
||||||
|
* [Theme Customization](site/docs/theme.md)
|
||||||
|
* [Change Log](CHANGELOG.md)
|
||||||
|
* [Examples](https://didi.github.io/mand-mobile/example/)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Install & Usage
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install mand-mobile --save
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import
|
||||||
|
|
||||||
|
* Use <a href="https://github.com/ant-design/babel-plugin-import" target="_blank">babel-plugin-import</a>
|
||||||
|
or
|
||||||
|
<a href="https://github.com/Brooooooklyn/ts-import-plugin" target="_blank">ts-import-plugin</a> (Recommended)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Button } from 'mand-mobile'
|
||||||
|
```
|
||||||
|
|
||||||
|
* Manually import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import Button from 'mand-mobile/lib/button'
|
||||||
|
import 'mand-mobile/lib/button/style'
|
||||||
|
```
|
||||||
|
|
||||||
|
* Totally import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import Vue from 'vue'
|
||||||
|
import mandMobile from 'mand-mobile'
|
||||||
|
import 'mand-mobile/lib/mand-mobile.css'
|
||||||
|
|
||||||
|
Vue.use(mandMobile)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Select the components that you need to build your webapp. Find more details in [component preview](/mfe/mand-mobile/docs/preview).
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<md-field title="form" class="block">
|
||||||
|
<md-input-item
|
||||||
|
title="name"
|
||||||
|
placeholder="Please input your name."
|
||||||
|
></md-input-item>
|
||||||
|
<md-field-item
|
||||||
|
title="sex"
|
||||||
|
arrow="arrow-right"
|
||||||
|
:value="sex"
|
||||||
|
@click="isPickerShow = true"
|
||||||
|
solid
|
||||||
|
></md-field-item>
|
||||||
|
<md-picker
|
||||||
|
v-model="isPickerShow"
|
||||||
|
:data="pickerData"
|
||||||
|
title="sex"
|
||||||
|
></md-picker>
|
||||||
|
</md-field>
|
||||||
|
<md-agree class="agree">
|
||||||
|
Agree to the privacy policy.
|
||||||
|
</md-agree>
|
||||||
|
<md-action-bar :actions="actionBarData">
|
||||||
|
¥128.00
|
||||||
|
</md-action-bar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Agree,
|
||||||
|
ActionBar,
|
||||||
|
Field,
|
||||||
|
FieldItem,
|
||||||
|
InputItem,
|
||||||
|
Picker
|
||||||
|
} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
[Agree.name]: Agree,
|
||||||
|
[ActionBar.name]: ActionBar,
|
||||||
|
[Field.name]: Field,
|
||||||
|
[FieldItem.name]: FieldItem,
|
||||||
|
[InputItem.name]: InputItem,
|
||||||
|
[Picker.name]: Picker
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isPickerShow: false,
|
||||||
|
actionBarData: [{
|
||||||
|
text: 'confirm'
|
||||||
|
}],
|
||||||
|
pickerData: [[{text:'male'},{text:'female'}]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,49 @@
|
||||||
|
'use strict'
|
||||||
|
const chalk = require('chalk')
|
||||||
|
const semver = require('semver')
|
||||||
|
const packageConfig = require('../package.json')
|
||||||
|
const shell = require('shelljs')
|
||||||
|
function exec (cmd) {
|
||||||
|
return require('child_process').execSync(cmd).toString().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionRequirements = [
|
||||||
|
{
|
||||||
|
name: 'node',
|
||||||
|
currentVersion: semver.clean(process.version),
|
||||||
|
versionRequirement: packageConfig.engines.node
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (shell.which('npm')) {
|
||||||
|
versionRequirements.push({
|
||||||
|
name: 'npm',
|
||||||
|
currentVersion: exec('npm --version'),
|
||||||
|
versionRequirement: packageConfig.engines.npm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function () {
|
||||||
|
const warnings = []
|
||||||
|
for (let i = 0; i < versionRequirements.length; i++) {
|
||||||
|
const mod = versionRequirements[i]
|
||||||
|
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||||
|
warnings.push(mod.name + ': ' +
|
||||||
|
chalk.red(mod.currentVersion) + ' should be ' +
|
||||||
|
chalk.green(mod.versionRequirement)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length) {
|
||||||
|
console.log('')
|
||||||
|
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||||
|
console.log()
|
||||||
|
for (let i = 0; i < warnings.length; i++) {
|
||||||
|
const warning = warnings[i]
|
||||||
|
console.log(' ' + warning)
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
const prompt = require('inquirer').createPromptModule()
|
||||||
|
const shellJs = require('shelljs')
|
||||||
|
const path = require('path')
|
||||||
|
const bluebird = require('bluebird')
|
||||||
|
const ora = require('ora')
|
||||||
|
const moment = require('moment')
|
||||||
|
const userName = require('git-user-name')
|
||||||
|
const userEmail = require('git-user-email')
|
||||||
|
const fs = bluebird.promisifyAll(require('fs'))
|
||||||
|
|
||||||
|
// path
|
||||||
|
const CWD = process.cwd()
|
||||||
|
const COMPONENTS_PATH = path.resolve(CWD, './components')
|
||||||
|
const EXPECT_SHELL = path.resolve(CWD, './build/template.exp')
|
||||||
|
const DEMO_INDEX_PATH = path.resolve(CWD, './examples/demo-index.js')
|
||||||
|
const COMPONENT_INDEX = path.resolve(CWD, './components/index.js')
|
||||||
|
const COMPONENT_JSON = path.resolve(CWD, './examples/components.json')
|
||||||
|
|
||||||
|
function init (answers) {
|
||||||
|
return Promise.resolve(answers)
|
||||||
|
.then(checkNoRepeat)
|
||||||
|
.then(create)
|
||||||
|
.then(sync)
|
||||||
|
.catch(err => {
|
||||||
|
console.warn(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNoRepeat(answers) {
|
||||||
|
return fs.readdirAsync(COMPONENTS_PATH)
|
||||||
|
.then(files => {
|
||||||
|
return Promise.all(files.map(file => checkFile(COMPONENTS_PATH, file, answers.componentName))).then(() => answers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function upperFirst(str) {
|
||||||
|
return String.prototype.replace.call(str, /^\w/, function (match) {
|
||||||
|
return String.prototype.toUpperCase.call(match)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeKebabToCamel(str) {
|
||||||
|
return String.prototype.replace.call(str, /\-(\w)/g, function(match, p1) {
|
||||||
|
return String.prototype.toUpperCase.call(p1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync(answers) {
|
||||||
|
return Promise.all([syncToComponentJson(answers), syncToExample(answers), syncToIndex(answers)]).then(() =>answers)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncToExample(answers) {
|
||||||
|
fs.readFileAsync(DEMO_INDEX_PATH, 'utf8')
|
||||||
|
.then(str => {
|
||||||
|
return compile(answers, str)
|
||||||
|
})
|
||||||
|
.then(str => fs.writeFileAsync(DEMO_INDEX_PATH, str, 'utf8'))
|
||||||
|
.then(() => answers)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncToIndex(answers) {
|
||||||
|
/* 同步components/index文件 */
|
||||||
|
fs.readFileAsync(COMPONENT_INDEX, 'utf8')
|
||||||
|
.then(str => {
|
||||||
|
return compile(answers, str)
|
||||||
|
})
|
||||||
|
.then(str => fs.writeFileAsync(COMPONENT_INDEX, str, 'utf8'))
|
||||||
|
.then(() => answers)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncToComponentJson(answers) {
|
||||||
|
const json = require(COMPONENT_JSON)
|
||||||
|
let index = json.findIndex(item => item.category === answers.componentType)
|
||||||
|
if (index === -1) {
|
||||||
|
index = json.length
|
||||||
|
json.push({
|
||||||
|
category: answers.componentType,
|
||||||
|
list: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
json[index].list.push({
|
||||||
|
name: answers.componentNameUpper,
|
||||||
|
path: `/${answers.componentName}`,
|
||||||
|
icon: answers.componentName,
|
||||||
|
text: answers.componentCnName,
|
||||||
|
})
|
||||||
|
Array.prototype.forEach.call(json, element => {
|
||||||
|
Array.prototype.sort.call(element.list, function(a, b) {
|
||||||
|
return a.name > b.name ? 1 : -1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Array.prototype.sort.call(json, (a, b) => {
|
||||||
|
return a.category > b.category ? 1: -1
|
||||||
|
})
|
||||||
|
return fs.writeFileAsync(COMPONENT_JSON, JSON.stringify(json), 'utf8')
|
||||||
|
.then(answers => answers)
|
||||||
|
}
|
||||||
|
|
||||||
|
function compile(metaData, fileStr) {
|
||||||
|
return String.prototype.replace.call(fileStr, /\/\*.*@init<%(.*)%>.*\*\//g, function (match, p1) {
|
||||||
|
return (String.prototype.replace.call(p1, /\${(\w*)}/g, function (innMatch, innP1) {
|
||||||
|
return metaData[innP1]
|
||||||
|
})) + '\r' +match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkFile(dir, file, name) {
|
||||||
|
const filePath = path.resolve(dir, `./${file}`)
|
||||||
|
return fs.statAsync(filePath)
|
||||||
|
.then(stat => {
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
if (name === file) {
|
||||||
|
return Promise.reject(`组件库中已经存在名为${name}的组件!请仔细核对后重新创建`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function exec (command, argvs) {
|
||||||
|
const spinner = ora('Loading...').start()
|
||||||
|
const result = shellJs.exec(`${command} ${argvs.map(item => `\'${item}\'`).join(' ')} >> /dev/null`)
|
||||||
|
spinner.succeed(['执行完毕'])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(answers) {
|
||||||
|
exec('expect', [EXPECT_SHELL, answers.componentCnName, answers.componentName, answers.componentType, answers.componentDesc, answers.author, answers.time, COMPONENTS_PATH])
|
||||||
|
return answers
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserInfo() {
|
||||||
|
let user = userName()
|
||||||
|
if (!user) {
|
||||||
|
user = 'anonymous'
|
||||||
|
}
|
||||||
|
const email = userEmail()
|
||||||
|
if (email) {
|
||||||
|
user += ` <${email}>`
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch() {
|
||||||
|
return prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'componentName',
|
||||||
|
message: '请输入要创建的组件名称(kebab-case):',
|
||||||
|
validate: function (str) {
|
||||||
|
return /^[a-z][a-z|-]*[a-z]$/.test(str)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'componentCnName',
|
||||||
|
message: '请输入要创建的组件中文名称(中文):',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
choices: [
|
||||||
|
"basic",
|
||||||
|
"feedback",
|
||||||
|
"form",
|
||||||
|
"business",
|
||||||
|
],
|
||||||
|
name: 'componentType',
|
||||||
|
message: '组件类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'componentDesc',
|
||||||
|
message: '组件描述',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'author',
|
||||||
|
message: '作者',
|
||||||
|
default: getUserInfo(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'time',
|
||||||
|
message: 'time',
|
||||||
|
default: moment().format('YYYY年MM月DD日')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.then(answers => {
|
||||||
|
answers = Object.assign(answers, {
|
||||||
|
componentNameUpper: upperFirst(changeKebabToCamel(answers.componentName))
|
||||||
|
})
|
||||||
|
return init(answers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
launch()
|
|
@ -0,0 +1,86 @@
|
||||||
|
const wrap = require('word-wrap')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
prompter: function(cz, commit) {
|
||||||
|
cz.prompt([
|
||||||
|
{
|
||||||
|
type: "list",
|
||||||
|
name: "type",
|
||||||
|
message: "select the type of change that you\'re committing",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: "feat: a feature addition (required)",
|
||||||
|
value: "feat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fix: fix a bug",
|
||||||
|
value: "fix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "doc: a document modify or addition",
|
||||||
|
value: "doc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build: The front-end engineering",
|
||||||
|
value: "build",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "example: Example for component",
|
||||||
|
value: "example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test: unit test for component",
|
||||||
|
value: "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "scoped",
|
||||||
|
message: "affected components for this commit, for example: button\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "description",
|
||||||
|
message: "Abstract of this commit, perfer English\n",
|
||||||
|
validate: function (str) {
|
||||||
|
return !!str
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "issue",
|
||||||
|
message: "Related issue number for this commit, please split with \'\,\'\n",
|
||||||
|
validate: function (str) {
|
||||||
|
return /(\d*\,)*\d*/.test(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
.then(answers => {
|
||||||
|
const maxLineWidth = 80;
|
||||||
|
|
||||||
|
const wrapOptions = {
|
||||||
|
trim: true,
|
||||||
|
newline: '\n',
|
||||||
|
indent:'',
|
||||||
|
width: maxLineWidth
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Hard limit this line
|
||||||
|
let body = answers.type
|
||||||
|
if (answers.scoped) {
|
||||||
|
body += `(${answers.scoped}): `
|
||||||
|
} else {
|
||||||
|
body += ': '
|
||||||
|
}
|
||||||
|
body += answers.description
|
||||||
|
if (answers.issue) {
|
||||||
|
const issue = answers.issue.split(',')
|
||||||
|
body += `[${issue.map(item => '#'+item).join(',')}]`
|
||||||
|
}
|
||||||
|
wrap(body, wrapOptions)
|
||||||
|
commit(body)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
const path = require('path')
|
||||||
|
const glob = require('glob')
|
||||||
|
const compiler = require('vueify').compiler
|
||||||
|
const bluebird = require('bluebird')
|
||||||
|
const fs = bluebird.promisifyAll(require('fs'))
|
||||||
|
const copy = require('recursive-copy')
|
||||||
|
const stylus = require('stylus')
|
||||||
|
const babel = bluebird.promisifyAll(require('babel-core'))
|
||||||
|
const TARGET_LIB_BASE = 'lib'
|
||||||
|
const SRC_BASE = 'components'
|
||||||
|
|
||||||
|
|
||||||
|
function babelPluginInsertCssImportForVue ({ types: t }) {
|
||||||
|
function computedSameDirCssPosition(filePath) {
|
||||||
|
const filePathParse = path.parse(filePath)
|
||||||
|
return `./style/${filePathParse.name}.css`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
Program(path, state) {
|
||||||
|
const importLiteral = computedSameDirCssPosition(state.opts.filePath)
|
||||||
|
path.unshiftContainer('body', t.ImportDeclaration([],t.StringLiteral(importLiteral)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileVueStylus (content, cb) {
|
||||||
|
stylus(content)
|
||||||
|
// .include(path.join(__dirname, 'src/*'))
|
||||||
|
.import(path.join(__dirname, '../../components/_style/mixin/*.styl'))
|
||||||
|
.import(path.join(__dirname, '../../node_modules/nib/lib/nib/vendor'))
|
||||||
|
.import(path.join(__dirname, '../../node_modules/nib/lib/nib/gradients'))
|
||||||
|
.import(path.join(__dirname, '../../node_modules/nib/lib/nib/flex'))
|
||||||
|
.render((err, css) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
cb(null, css)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function computedCompilerConfig(filePath) {
|
||||||
|
return {
|
||||||
|
extractCSS: true,
|
||||||
|
babel: {
|
||||||
|
plugins: [
|
||||||
|
[babelPluginInsertCssImportForVue, {
|
||||||
|
filePath,
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
customCompilers: {
|
||||||
|
stylus: compileVueStylus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(destDir) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
copy(SRC_BASE, destDir, {filter: function(item) {
|
||||||
|
if (/demo|test/.test(item)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}}, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileVueAndReplace(filePath) {
|
||||||
|
const styleDir = path.join(path.dirname(filePath), 'style')
|
||||||
|
if (!fs.existsSync(styleDir)) {
|
||||||
|
fs.mkdirSync(styleDir)
|
||||||
|
}
|
||||||
|
const fileBaseName = path.basename(filePath, '.vue')
|
||||||
|
const cssFilePath = path.join(styleDir, `${fileBaseName}.css`)
|
||||||
|
const jsFilePath = filePath.replace(/\.vue$/, '.js')
|
||||||
|
console.info(cssFilePath, jsFilePath)
|
||||||
|
const fileContent = fs.readFileSync(filePath, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
})
|
||||||
|
const config = computedCompilerConfig(filePath)
|
||||||
|
compiler.applyConfig(config)
|
||||||
|
let styleContent = ''
|
||||||
|
const styleCb = res => {
|
||||||
|
if (res.style) {
|
||||||
|
styleContent = res.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compiler.on('style', styleCb)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
compiler.compile(fileContent, filePath, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
compiler.removeListener('style', styleCb)
|
||||||
|
fs.writeFileAsync(jsFilePath, result)
|
||||||
|
.then(() => fs.writeFileAsync(cssFilePath, styleContent))
|
||||||
|
.then(() => {
|
||||||
|
return fs.unlinkAsync(filePath)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileJsAndReplace(filePath){
|
||||||
|
babel.transformFileAsync(filePath)
|
||||||
|
.then(({code}) => {
|
||||||
|
return fs.writeFileAsync(filePath, code)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.info(`${filePath} build error::error.stack=${error.stack}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileAndReplaceAllJsFile() {
|
||||||
|
const fileGlob = `${TARGET_LIB_BASE}/**/*.js`
|
||||||
|
const jsFiles = glob.sync(fileGlob)
|
||||||
|
return Promise.all(jsFiles.map(compileJsAndReplace))
|
||||||
|
.catch(e => {
|
||||||
|
console.info(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileAndReplaceAllVueFile() {
|
||||||
|
const fileGlob = `${TARGET_LIB_BASE}/**/*.vue`
|
||||||
|
const jsFiles = glob.sync(fileGlob)
|
||||||
|
return Promise.all(jsFiles.map(compileVueAndReplace))
|
||||||
|
.catch(e => {
|
||||||
|
console.info(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
return move('lib')
|
||||||
|
.then(() => Promise.all([compileAndReplaceAllJsFile(), compileAndReplaceAllVueFile()]))
|
||||||
|
.catch(e => console.info(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
|
@ -0,0 +1,28 @@
|
||||||
|
const { rollupPlugin, EXAMPLE_OUTPUT_DIR, PROJECT_DIR } = require('./rollup-plugin-config')
|
||||||
|
const rollup = require('rollup')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const inputOptions = {
|
||||||
|
input: path.resolve(PROJECT_DIR, 'examples/main.indemand.js'),
|
||||||
|
plugins: rollupPlugin,
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputCommonjsOptions = {
|
||||||
|
file: path.resolve(EXAMPLE_OUTPUT_DIR, 'mand-mobile-example.js'),
|
||||||
|
format: 'umd',
|
||||||
|
}
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
return rollup.rollup(inputOptions)
|
||||||
|
.then(bundle => {
|
||||||
|
return bundle.write(outputCommonjsOptions).then(() => {
|
||||||
|
console.info('build example succ')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.info(err)
|
||||||
|
console.info('build error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
build()
|
|
@ -0,0 +1,43 @@
|
||||||
|
const rollup = require('rollup')
|
||||||
|
const path = require('path')
|
||||||
|
const {
|
||||||
|
LIB_DIR,
|
||||||
|
PROJECT_DIR,
|
||||||
|
rollupPlugin
|
||||||
|
} = require('./rollup-plugin-config')
|
||||||
|
|
||||||
|
const inputOptions = {
|
||||||
|
input: path.resolve(PROJECT_DIR, 'components/index.js'),
|
||||||
|
external: ['vue'],
|
||||||
|
plugins: rollupPlugin
|
||||||
|
}
|
||||||
|
const outputUmdOptions = {
|
||||||
|
file: path.resolve(LIB_DIR, 'mand-mobile.umd.js'),
|
||||||
|
format: 'umd',
|
||||||
|
name: 'mand-mobile'
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputEsOptions = {
|
||||||
|
file: path.resolve(LIB_DIR, 'mand-mobile.esm.js'),
|
||||||
|
format: 'es',
|
||||||
|
}
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
return rollup.rollup(inputOptions)
|
||||||
|
.then(bundle => {
|
||||||
|
return Promise.all([
|
||||||
|
bundle.write(outputUmdOptions).then(() => {
|
||||||
|
console.info('build umd module succ')
|
||||||
|
}),
|
||||||
|
bundle.write(outputEsOptions).then(() => {
|
||||||
|
console.info('build es module succ')
|
||||||
|
})
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.info(err)
|
||||||
|
console.info('build error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
build()
|
|
@ -0,0 +1,65 @@
|
||||||
|
const rollup = require('rollup')
|
||||||
|
const path = require('path')
|
||||||
|
const { rollupPlugin, DEV_OUTPUT_DIR, PROJECT_DIR } = require('./rollup-plugin-config')
|
||||||
|
|
||||||
|
|
||||||
|
// express
|
||||||
|
const livereload = require('livereload')
|
||||||
|
const express = require('express')
|
||||||
|
const history = require('connect-history-api-fallback')
|
||||||
|
// const opn = require('opn')
|
||||||
|
const port = 4000
|
||||||
|
|
||||||
|
const inputOptions = {
|
||||||
|
input: path.resolve(PROJECT_DIR, 'examples/main.indemand.js'),
|
||||||
|
plugins: rollupPlugin,
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputCommonjsOptions = {
|
||||||
|
file: path.resolve(DEV_OUTPUT_DIR, 'mand-mobile-dev.js'),
|
||||||
|
format: 'umd',
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch() {
|
||||||
|
const watchOptions = {
|
||||||
|
...inputOptions,
|
||||||
|
output: outputCommonjsOptions,
|
||||||
|
}
|
||||||
|
const watcher = rollup.watch(watchOptions)
|
||||||
|
watcher.on('event', e => {
|
||||||
|
console.info(e)
|
||||||
|
if (e.code === 'END') {
|
||||||
|
console.info('resource rebuild')
|
||||||
|
}
|
||||||
|
if (e.code === 'ERROR') {
|
||||||
|
console.info(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function serve(path) {
|
||||||
|
return express.static(path, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function runServer() {
|
||||||
|
// rollup buildwatch
|
||||||
|
watch()
|
||||||
|
|
||||||
|
// livereload
|
||||||
|
const lrserver = livereload.createServer()
|
||||||
|
lrserver.watch(path.join(process.cwd(), 'output'))
|
||||||
|
|
||||||
|
// static resource server
|
||||||
|
const app = express()
|
||||||
|
app.use(history({
|
||||||
|
verbose: true
|
||||||
|
}))
|
||||||
|
app.use('/', serve(DEV_OUTPUT_DIR))
|
||||||
|
app.listen(port, function() {
|
||||||
|
console.log('> Starting dev server...')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
runServer()
|
|
@ -0,0 +1,159 @@
|
||||||
|
const path = require('path')
|
||||||
|
const os = require('os')
|
||||||
|
const fs = require('fs')
|
||||||
|
const aliasPlugin = require('rollup-plugin-alias')
|
||||||
|
const replacePlugin = require('rollup-plugin-replace')
|
||||||
|
const jsonPlugin = require('rollup-plugin-json')
|
||||||
|
const urlPlugin = require('rollup-plugin-url')
|
||||||
|
const nodeResolvePlugin = require('rollup-plugin-node-resolve')
|
||||||
|
const vuePlugin = require('rollup-plugin-vue')
|
||||||
|
const babel = require('rollup-plugin-babel')
|
||||||
|
const common = require('rollup-plugin-commonjs')
|
||||||
|
const stylusMixin = require('../stylus-mixin')
|
||||||
|
const builtins = require('rollup-plugin-node-builtins')
|
||||||
|
const uglify = require('rollup-plugin-uglify')
|
||||||
|
const nodeGlobals = require('rollup-plugin-node-globals')
|
||||||
|
const glob = require('rollup-plugin-glob-import')
|
||||||
|
const progress = require('rollup-plugin-progress')
|
||||||
|
const fillHtmlPlugin = require('rollup-plugin-template-html')
|
||||||
|
const filesize = require('rollup-plugin-filesize')
|
||||||
|
const postcss = require('rollup-plugin-postcss')
|
||||||
|
// const postcssUrl = require('postcss-url')
|
||||||
|
|
||||||
|
const babelrc = require('babelrc-rollup').default
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
|
const isTest = process.env.NODE_ENV === 'testing'
|
||||||
|
const isDev = !(isProduction || isTest)
|
||||||
|
const isExample = process.env.BUILD_TYPE === 'example'
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.resolve(__dirname, '../..', dir)
|
||||||
|
}
|
||||||
|
const LIB_DIR = resolve('lib')
|
||||||
|
const PROJECT_DIR = resolve('.')
|
||||||
|
|
||||||
|
const tmpDir = os.tmpdir()
|
||||||
|
const DEV_OUTPUT_DIR = fs.mkdtempSync(`${tmpDir}${path.sep}`)
|
||||||
|
const EXAMPLE_OUTPUT_DIR = resolve('docs/example')
|
||||||
|
|
||||||
|
const tmpTestDir = os.tmpdir()
|
||||||
|
const TEST_OUTPUT_DIR = fs.mkdtempSync(`${tmpTestDir}${path.sep}`)
|
||||||
|
|
||||||
|
function vueWarpper() {
|
||||||
|
let distDir = '', fileName = ''
|
||||||
|
if (isDev) {
|
||||||
|
distDir = DEV_OUTPUT_DIR
|
||||||
|
fileName = 'mand-mobile-dev.css'
|
||||||
|
} else if (isExample) {
|
||||||
|
distDir = EXAMPLE_OUTPUT_DIR
|
||||||
|
fileName = 'mand-mobile-example.css'
|
||||||
|
} else if (isProduction) {
|
||||||
|
distDir = LIB_DIR
|
||||||
|
fileName = 'mand-mobile.css'
|
||||||
|
} else if (isTest) {
|
||||||
|
distDir = TEST_OUTPUT_DIR
|
||||||
|
fileName = 'mand-mobile-test.css'
|
||||||
|
}
|
||||||
|
return vuePlugin({
|
||||||
|
css: path.resolve(distDir, fileName),
|
||||||
|
stylus: {
|
||||||
|
use: [stylusMixin],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const vue = vueWarpper()
|
||||||
|
// const css = cssWarpper()
|
||||||
|
|
||||||
|
function conditionHelper(condition, plugins) {
|
||||||
|
return condition ? plugins : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const rollupPlugin = [
|
||||||
|
// resolve
|
||||||
|
...(conditionHelper(!isDev, [
|
||||||
|
aliasPlugin({
|
||||||
|
resolve: ['.js', '/index.js', '.css', '.vue', '.svg'], // @TODO '/index.js' hack
|
||||||
|
'mand-mobile/components': resolve('components'),
|
||||||
|
'mand-mobile/lib': resolve('lib'),
|
||||||
|
'mand-mobile': resolve('lib/mand-mobile.esm.js'),
|
||||||
|
'@examples/assets/images/bank-zs.svg': resolve('examples/assets/images/bank-zs.svg')
|
||||||
|
}),
|
||||||
|
])),
|
||||||
|
...(conditionHelper(isDev, [
|
||||||
|
aliasPlugin({
|
||||||
|
resolve: ['.js', '/index.js', '.css', '.vue', '.svg'], // @TODO '/index.js' hack
|
||||||
|
'mand-mobile/components': resolve('components'),
|
||||||
|
'mand-mobile/lib': resolve('lib'),
|
||||||
|
'mand-mobile': resolve('components'),
|
||||||
|
'@examples/assets/images/bank-zs.svg': resolve('examples/assets/images/bank-zs.svg')
|
||||||
|
}),
|
||||||
|
])),
|
||||||
|
nodeResolvePlugin({
|
||||||
|
extensions: [ '.js', '.json', '.vue' ],
|
||||||
|
}),
|
||||||
|
...(conditionHelper(isTest, [
|
||||||
|
common({
|
||||||
|
exclude: ['components/_util/*.js'],
|
||||||
|
namedExports: { 'avoriaz': ['mount', 'shallow'] },
|
||||||
|
}),
|
||||||
|
glob(),
|
||||||
|
])),
|
||||||
|
|
||||||
|
// inject
|
||||||
|
replacePlugin({
|
||||||
|
'process.env.NODE_ENV': `"${process.env.NODE_ENV}"`
|
||||||
|
}),
|
||||||
|
...(conditionHelper(isTest, [
|
||||||
|
builtins(),
|
||||||
|
nodeGlobals(),
|
||||||
|
])),
|
||||||
|
|
||||||
|
// resource
|
||||||
|
urlPlugin({
|
||||||
|
limit: 10 * 1024,
|
||||||
|
}),
|
||||||
|
jsonPlugin(),
|
||||||
|
vue,
|
||||||
|
postcss(),
|
||||||
|
babel(babelrc({
|
||||||
|
addModuleOptions: false,
|
||||||
|
findRollupPresets: true,
|
||||||
|
addExternalHelpersPlugin: false,
|
||||||
|
})),
|
||||||
|
|
||||||
|
// dest
|
||||||
|
...(conditionHelper(isProduction, [
|
||||||
|
uglify({
|
||||||
|
compress: {},
|
||||||
|
}),
|
||||||
|
])),
|
||||||
|
...(conditionHelper(isDev, [
|
||||||
|
fillHtmlPlugin({
|
||||||
|
template: resolve('examples/index.html'),
|
||||||
|
publicPath: '/',
|
||||||
|
destFile: path.resolve(DEV_OUTPUT_DIR, 'index.html')
|
||||||
|
})
|
||||||
|
])),
|
||||||
|
...(conditionHelper(isExample, [
|
||||||
|
fillHtmlPlugin({
|
||||||
|
template: resolve('examples/index.html'),
|
||||||
|
publicPath: '/',
|
||||||
|
destFile: path.resolve(EXAMPLE_OUTPUT_DIR, 'index.html')
|
||||||
|
})
|
||||||
|
])),
|
||||||
|
// cli
|
||||||
|
progress(),
|
||||||
|
...(conditionHelper(isProduction, [
|
||||||
|
filesize(),
|
||||||
|
])),
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LIB_DIR,
|
||||||
|
PROJECT_DIR,
|
||||||
|
EXAMPLE_OUTPUT_DIR,
|
||||||
|
DEV_OUTPUT_DIR,
|
||||||
|
rollupPlugin,
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
const path = require('path')
|
||||||
|
module.exports = function useMixin(style) {
|
||||||
|
return style
|
||||||
|
.import(path.join(__dirname, '../components/_style/mixin/*.styl'))
|
||||||
|
.import(path.join(__dirname, '../node_modules/nib/lib/nib/vendor'))
|
||||||
|
.import(path.join(__dirname, '../node_modules/nib/lib/nib/gradients'))
|
||||||
|
.import(path.join(__dirname, '../node_modules/nib/lib/nib/flex'))
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/expect
|
||||||
|
|
||||||
|
set timeout 30
|
||||||
|
set chinese_name [lindex $argv 0]
|
||||||
|
set component_name [lindex $argv 1]
|
||||||
|
set component_type [lindex $argv 2]
|
||||||
|
set component_desc [lindex $argv 3]
|
||||||
|
set author [lindex $argv 4]
|
||||||
|
set create_time [lindex $argv 5]
|
||||||
|
set component_path [lindex $argv 6]
|
||||||
|
spawn mfe init mfe-template-mfd-mobile "$component_path/${component_name}"
|
||||||
|
|
||||||
|
expect "组件中文名称,如:轻提示"
|
||||||
|
|
||||||
|
send "${chinese_name}\r"
|
||||||
|
|
||||||
|
expect "组件类型, 选项: basic feedback form business"
|
||||||
|
|
||||||
|
send "${component_type}\r"
|
||||||
|
|
||||||
|
expect "组件描述"
|
||||||
|
|
||||||
|
send "${component_desc}\r"
|
||||||
|
|
||||||
|
expect "Author"
|
||||||
|
|
||||||
|
send "${author}\r"
|
||||||
|
|
||||||
|
expect "name"
|
||||||
|
|
||||||
|
send "${component_name}\r"
|
||||||
|
|
||||||
|
expect "time"
|
||||||
|
|
||||||
|
send "${create_time}\r"
|
||||||
|
|
||||||
|
expect eof
|
||||||
|
|
||||||
|
exit
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
'use strict'
|
||||||
|
require('../check-versions')()
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
|
||||||
|
// // const ora = require('ora')
|
||||||
|
const rm = require('rimraf')
|
||||||
|
const path = require('path')
|
||||||
|
const chalk = require('chalk')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const config = require('../../config')
|
||||||
|
const webpackConfig = require('./webpack.example.conf')
|
||||||
|
|
||||||
|
|
||||||
|
rm(path.join(config.example.assetsRoot, config.example.assetsSubDirectory), err => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
webpack(webpackConfig, function (err, stats) {
|
||||||
|
// spinner.stop()
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
process.stdout.write(stats.toString({
|
||||||
|
colors: true,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false
|
||||||
|
}) + '\n\n')
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
console.log(chalk.red(' Build failed with errors.\n'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.cyan(' Build complete.\n'))
|
||||||
|
console.log(chalk.yellow(
|
||||||
|
' Tip: built files are meant to be served over an HTTP server.\n' +
|
||||||
|
' Opening index.html over file:// won\'t work.\n'
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,28 @@
|
||||||
|
'use strict'
|
||||||
|
require('../check-versions')()
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'production'
|
||||||
|
|
||||||
|
const chalk = require('chalk')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const webpackConfig = require('./webpack.build.conf')
|
||||||
|
|
||||||
|
webpack(webpackConfig, function (err, stats) {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
process.stdout.write(stats.toString({
|
||||||
|
colors: true,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false
|
||||||
|
}) + '\n\n')
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
console.log(chalk.red(' Build failed with errors.\n'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.cyan(' Build complete.\n'))
|
||||||
|
})
|
|
@ -0,0 +1,67 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build style entry of all components
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const path = require('path')
|
||||||
|
const components = require('../../examples/components.json')
|
||||||
|
const dependencyTree = require('dependency-tree')
|
||||||
|
const libDir = path.resolve(__dirname, '../../lib')
|
||||||
|
|
||||||
|
const SEP = path.sep
|
||||||
|
|
||||||
|
function generateComponentsList (components) {
|
||||||
|
const list = ['field-item', 'swiper-item', 'popup-title-bar']
|
||||||
|
components.map(nav =>
|
||||||
|
nav.list.map(item =>
|
||||||
|
list.push(item.path.substr(1))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentList = generateComponentsList(components)
|
||||||
|
function checkComponentHasStyle(componentName) {
|
||||||
|
if (~componentName.indexOf('.js')) {
|
||||||
|
componentName = componentName.replace('.js', '.css')
|
||||||
|
return fs.existsSync(path.join(__dirname, `../lib/style/${componentName}`))
|
||||||
|
} else {
|
||||||
|
return fs.existsSync(path.join(__dirname, `../lib/style/${componentName}/index.css`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(tree, checkList) {
|
||||||
|
tree && Object.keys(tree).forEach(key => {
|
||||||
|
search(tree[key], checkList)
|
||||||
|
const component = key.split(`${SEP}mand-mobile${SEP}lib${SEP}`)[1].replace(`${SEP}index.js`, '').replace(`mixins${SEP}`, '')
|
||||||
|
if (checkList.indexOf(component) === -1) {
|
||||||
|
checkList.push(component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze component dependencies
|
||||||
|
function analyzeDependencies(componentName, libDir) {
|
||||||
|
const checkList = []
|
||||||
|
search(dependencyTree({
|
||||||
|
directory: libDir,
|
||||||
|
filename: path.resolve(libDir, componentName, 'index.js'),
|
||||||
|
filter: path => path.indexOf(`mand-mobile${SEP}lib${SEP}`) !== -1
|
||||||
|
}), checkList)
|
||||||
|
return checkList.filter(component => checkComponentHasStyle(component))
|
||||||
|
}
|
||||||
|
|
||||||
|
componentList.forEach(componentName => {
|
||||||
|
const content = analyzeDependencies(componentName, libDir).map(component => {
|
||||||
|
if (~component.indexOf('.js')) {
|
||||||
|
component = component.replace('.js', '.css')
|
||||||
|
return `require('../../style/${component}')`
|
||||||
|
} else {
|
||||||
|
return `require('../../style/${component}/index.css')`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
content.unshift('require(\'../../style/global.css\')')
|
||||||
|
fs.outputFileSync(path.join(libDir, componentName, './style/index.js'), content.join('\n'))
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
'use strict'
|
||||||
|
require('eventsource-polyfill')
|
||||||
|
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||||
|
|
||||||
|
hotClient.subscribe(function (event) {
|
||||||
|
if (event.action === 'reload') {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use strict'
|
||||||
|
require('../check-versions')()
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
if (!process.env.NODE_ENV) {
|
||||||
|
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
|
||||||
|
}
|
||||||
|
|
||||||
|
const opn = require('opn')
|
||||||
|
const path = require('path')
|
||||||
|
const express = require('express')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const proxyMiddleware = require('http-proxy-middleware')
|
||||||
|
let webpackConfig = require('./webpack.dev.conf')
|
||||||
|
// default port where dev server listens for incoming traffic
|
||||||
|
const port = process.env.PORT || config.dev.port
|
||||||
|
// automatically open browser, if not set will be false
|
||||||
|
const autoOpenBrowser = !!config.dev.autoOpenBrowser
|
||||||
|
// Define HTTP proxies to your custom API backend
|
||||||
|
// https://github.com/chimurai/http-proxy-middleware
|
||||||
|
const proxyTable = config.dev.proxyTable
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const compiler = webpack(webpackConfig)
|
||||||
|
const resolve = file => path.resolve(__dirname, file)
|
||||||
|
const devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||||
|
publicPath: webpackConfig.output.publicPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
const hotMiddleware = require('webpack-hot-middleware')(compiler, {
|
||||||
|
log: false,
|
||||||
|
heartbeat: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
const serve = function (path) {
|
||||||
|
return express.static(resolve(path), {})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// force page reload when html-webpack-plugin template changes
|
||||||
|
// currently disabled until this is resolved:
|
||||||
|
// https://github.com/jantimon/html-webpack-plugin/issues/680
|
||||||
|
// compiler.plugin('compilation', function (compilation) {
|
||||||
|
// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||||
|
// hotMiddleware.publish({ action: 'reload' })
|
||||||
|
// cb()
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// enable hot-reload and state-preserving
|
||||||
|
// compilation error display
|
||||||
|
app.use(hotMiddleware)
|
||||||
|
|
||||||
|
// proxy api requests
|
||||||
|
Object.keys(proxyTable).forEach(function (context) {
|
||||||
|
let options = proxyTable[context]
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
options = { target: options }
|
||||||
|
}
|
||||||
|
app.use(proxyMiddleware(options.filter || context, options))
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle fallback for HTML5 history API
|
||||||
|
app.use(require('connect-history-api-fallback')())
|
||||||
|
|
||||||
|
// serve webpack bundle output
|
||||||
|
app.use(devMiddleware)
|
||||||
|
app.use('/static', serve(path.join(__dirname, '../../static')))
|
||||||
|
// serve pure static assets
|
||||||
|
// const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||||
|
// app.use(staticPath, express.static('./static'))
|
||||||
|
|
||||||
|
var _resolve
|
||||||
|
var _reject
|
||||||
|
var readyPromise = new Promise((resolve, reject) => {
|
||||||
|
_resolve = resolve
|
||||||
|
_reject = reject
|
||||||
|
})
|
||||||
|
|
||||||
|
var server
|
||||||
|
var portfinder = require('portfinder')
|
||||||
|
portfinder.basePort = port
|
||||||
|
|
||||||
|
console.log('> Starting dev server...')
|
||||||
|
devMiddleware.waitUntilValid(() => {
|
||||||
|
portfinder.getPort((err, port) => {
|
||||||
|
if (err) {
|
||||||
|
_reject(err)
|
||||||
|
}
|
||||||
|
process.env.PORT = port
|
||||||
|
var uri = 'http://localhost:' + port
|
||||||
|
console.log('> Listening at ' + uri + '\n')
|
||||||
|
// when env is testing, don't need open it
|
||||||
|
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
|
||||||
|
opn(uri)
|
||||||
|
}
|
||||||
|
server = app.listen(port)
|
||||||
|
_resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ready: readyPromise,
|
||||||
|
close: () => {
|
||||||
|
server.close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
'use strict'
|
||||||
|
const path = require('path')
|
||||||
|
const config = require('../../config')
|
||||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||||
|
const resolve = file => path.resolve(__dirname, file)
|
||||||
|
|
||||||
|
exports.assetsPath = function (_path) {
|
||||||
|
const assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||||
|
? config.build.assetsSubDirectory
|
||||||
|
: config.dev.assetsSubDirectory
|
||||||
|
return path.posix.join(assetsSubDirectory, _path)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.cssLoaders = function (options) {
|
||||||
|
options = options || {}
|
||||||
|
|
||||||
|
const cssLoader = {
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
minimize: process.env.NODE_ENV === 'production',
|
||||||
|
sourceMap: options.sourceMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate loader string to be used with extract text plugin
|
||||||
|
function generateLoaders (loader, loaderOptions) {
|
||||||
|
const loaders = [cssLoader]
|
||||||
|
if (loader) {
|
||||||
|
loaders.push({
|
||||||
|
loader: loader + '-loader',
|
||||||
|
options: Object.assign({}, loaderOptions, {
|
||||||
|
sourceMap: options.sourceMap
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CSS when that option is specified
|
||||||
|
// (which is the case during production build)
|
||||||
|
if (options.extract) {
|
||||||
|
return ExtractTextPlugin.extract({
|
||||||
|
use: loaders,
|
||||||
|
fallback: 'vue-style-loader'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return ['vue-style-loader'].concat(loaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||||
|
const stylusMixins = [
|
||||||
|
'~nib/lib/nib/vendor',
|
||||||
|
'~nib/lib/nib/gradients.styl',
|
||||||
|
'~nib/lib/nib/flex',
|
||||||
|
resolve('../../components/_style/mixin/util.styl'),
|
||||||
|
resolve('../../components/_style/mixin/theme.styl'),
|
||||||
|
resolve('../../examples/theme.custom.styl')
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
css: generateLoaders(),
|
||||||
|
postcss: generateLoaders(),
|
||||||
|
stylus: generateLoaders('stylus', {
|
||||||
|
import: stylusMixins
|
||||||
|
}),
|
||||||
|
styl: generateLoaders('stylus', {
|
||||||
|
import: stylusMixins
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate loaders for standalone style files (outside of .vue)
|
||||||
|
exports.styleLoaders = function (options) {
|
||||||
|
const output = []
|
||||||
|
const loaders = exports.cssLoaders(options)
|
||||||
|
for (const extension in loaders) {
|
||||||
|
if (loaders.hasOwnProperty(extension)) {
|
||||||
|
const loader = loaders[extension]
|
||||||
|
output.push({
|
||||||
|
test: new RegExp('\\.' + extension + '$'),
|
||||||
|
use: loader
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict'
|
||||||
|
const utils = require('./utils')
|
||||||
|
const config = require('../../config')
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loaders: utils.cssLoaders({
|
||||||
|
sourceMap: isProduction
|
||||||
|
? config.build.productionSourceMap
|
||||||
|
: config.dev.cssSourceMap,
|
||||||
|
extract: isProduction
|
||||||
|
}),
|
||||||
|
transformToRequire: {
|
||||||
|
video: 'src',
|
||||||
|
source: 'src',
|
||||||
|
img: 'src',
|
||||||
|
image: 'xlink:href'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
'use strict'
|
||||||
|
const path = require('path')
|
||||||
|
const utils = require('./utils')
|
||||||
|
const vueLoaderConfig = require('./vue-loader.conf')
|
||||||
|
|
||||||
|
function resolve (dir) {
|
||||||
|
return path.join(__dirname, '../..', dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.vue', '.json'],
|
||||||
|
alias: {
|
||||||
|
'vue$': 'vue/dist/vue.runtime.esm.js',
|
||||||
|
'@examples': resolve('examples'),
|
||||||
|
'mand-mobile/lib': resolve('components'),
|
||||||
|
'mand-mobile/components': resolve('components'),
|
||||||
|
'mand-mobile': resolve('components'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|vue)$/,
|
||||||
|
loader: 'eslint-loader',
|
||||||
|
enforce: 'pre',
|
||||||
|
include: [resolve('components'), resolve('examples'), resolve('test')],
|
||||||
|
options: {
|
||||||
|
quiet: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
options: vueLoaderConfig
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel-loader?cacheDirectory',
|
||||||
|
include: [resolve('components'), resolve('examples'), resolve('test')]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpe?g|gif)(\?.*)?$/,
|
||||||
|
loader: 'url-loader',
|
||||||
|
options: {
|
||||||
|
limit: 10000,
|
||||||
|
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
loader: 'svg-sprite-loader',
|
||||||
|
include: [resolve('components'), resolve('examples/assets/images')]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
'use strict'
|
||||||
|
const utils = require('./utils')
|
||||||
|
const config = require('../../config')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const merge = require('webpack-merge')
|
||||||
|
const baseWebpackConfig = require('./webpack.base.conf')
|
||||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||||
|
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||||
|
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||||
|
const pkg = require('../../package.json')
|
||||||
|
|
||||||
|
const env = config.build.env
|
||||||
|
|
||||||
|
const webpackConfig = merge(baseWebpackConfig, {
|
||||||
|
entry: {
|
||||||
|
'mand-mobile': './components/index.js',
|
||||||
|
},
|
||||||
|
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||||
|
output: {
|
||||||
|
path: config.build.assetsRoot,
|
||||||
|
filename: utils.assetsPath('[name].js'),
|
||||||
|
library: 'mand-mobile',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
umdNamedDefine: true
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: utils.styleLoaders({ sourceMap: config.build.cssSourceMap })
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
vue: {
|
||||||
|
root: 'Vue',
|
||||||
|
commonjs: 'vue',
|
||||||
|
commonjs2: 'vue',
|
||||||
|
amd: 'vue'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new ProgressBarPlugin({
|
||||||
|
format: ' BUILD MAND_MOBILE [:bar] :percent (:elapsed seconds)',
|
||||||
|
clear: false
|
||||||
|
}),
|
||||||
|
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': env,
|
||||||
|
'MAN_VERSION': `'${pkg.version}'`
|
||||||
|
}),
|
||||||
|
// UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
compress: {
|
||||||
|
warnings: false
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
ascii_only: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// extract css into its own file
|
||||||
|
new ExtractTextPlugin({
|
||||||
|
filename: utils.assetsPath('[name].css')
|
||||||
|
}),
|
||||||
|
// Compress extracted CSS. We are using this plugin so that possible
|
||||||
|
// duplicated CSS from different components can be deduped.
|
||||||
|
new OptimizeCSSPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
safe: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new webpack.optimize.ModuleConcatenationPlugin()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.npm_package_config_analysis) {
|
||||||
|
webpackConfig.plugins.push(new BundleAnalyzerPlugin({
|
||||||
|
// Can be `server`, `static` or `disabled`.
|
||||||
|
// In `server` mode analyzer will start HTTP server to show bundle report.
|
||||||
|
// In `static` mode single HTML file with bundle report will be generated.
|
||||||
|
// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`.
|
||||||
|
analyzerMode: 'server',
|
||||||
|
// Host that will be used in `server` mode to start HTTP server.
|
||||||
|
analyzerHost: 'localhost',
|
||||||
|
// Port that will be used in `server` mode to start HTTP server.
|
||||||
|
analyzerPort: 8888,
|
||||||
|
// Path to bundle report file that will be generated in `static` mode.
|
||||||
|
// Relative to bundles output directory.
|
||||||
|
reportFilename: 'report.html',
|
||||||
|
// Module sizes to show in report by default.
|
||||||
|
// Should be one of `stat`, `parsed` or `gzip`.
|
||||||
|
// See "Definitions" section for more information.
|
||||||
|
defaultSizes: 'parsed',
|
||||||
|
// Automatically open report in default browser
|
||||||
|
openAnalyzer: true,
|
||||||
|
// If `true`, Webpack Stats JSON file will be generated in bundles output directory
|
||||||
|
generateStatsFile: false,
|
||||||
|
// Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
|
||||||
|
// Relative to bundles output directory.
|
||||||
|
statsFilename: 'stats.json',
|
||||||
|
// Options for `stats.toJson()` method.
|
||||||
|
// For example you can exclude sources of your modules from stats file with `source: false` option.
|
||||||
|
// See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
|
||||||
|
statsOptions: null,
|
||||||
|
// Log level. Can be 'info', 'warn', 'error' or 'silent'.
|
||||||
|
logLevel: 'info'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = webpackConfig
|
|
@ -0,0 +1,58 @@
|
||||||
|
const utils = require('./utils')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const config = require('../../config')
|
||||||
|
const merge = require('webpack-merge')
|
||||||
|
const baseWebpackConfig = require('./webpack.base.conf')
|
||||||
|
const poststylus = require('poststylus')
|
||||||
|
const pxtorem = require('postcss-pxtorem')
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||||
|
const DashBoardPlugin = require('webpack-dashboard/plugin')
|
||||||
|
const pkg = require('../../package.json')
|
||||||
|
// add hot-reload related code to entry chunks
|
||||||
|
// Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||||
|
// baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||||
|
// })
|
||||||
|
const pxtoremConfig = pxtorem({ rootValue: 100, propWhiteList: [] })
|
||||||
|
|
||||||
|
module.exports = merge(baseWebpackConfig, {
|
||||||
|
entry: {
|
||||||
|
'index': ['./build/webpack/dev-client', './examples/main.js']
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: config.dev.assetsRoot,
|
||||||
|
filename: '[name].js',
|
||||||
|
chunkFilename: '[name].js',
|
||||||
|
publicPath: config.dev.assetsPublicPath
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||||
|
},
|
||||||
|
// cheap-module-eval-source-map is faster for development
|
||||||
|
devtool: '#cheap-module-eval-source-map',
|
||||||
|
plugins: [
|
||||||
|
new DashBoardPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': config.dev.env,
|
||||||
|
'MAN_VERSION': `'${pkg.version}'`
|
||||||
|
}),
|
||||||
|
// https://github.com/seaneking/poststylus#webpack
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
options: {
|
||||||
|
stylus: {
|
||||||
|
use: [poststylus(pxtoremConfig)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
|
// https://github.com/ampedandwired/html-webpack-plugin
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: config.dev.index,
|
||||||
|
template: './examples/index.html',
|
||||||
|
inject: true
|
||||||
|
}),
|
||||||
|
new FriendlyErrorsPlugin()
|
||||||
|
]
|
||||||
|
})
|
|
@ -0,0 +1,142 @@
|
||||||
|
'use strict'
|
||||||
|
const path = require('path')
|
||||||
|
const utils = require('./utils')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const config = require('../../config')
|
||||||
|
const merge = require('webpack-merge')
|
||||||
|
const poststylus = require('poststylus')
|
||||||
|
const pxtorem = require('postcss-pxtorem')
|
||||||
|
const baseWebpackConfig = require('./webpack.base.conf')
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||||
|
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||||
|
|
||||||
|
const env = config.example.env
|
||||||
|
|
||||||
|
const webpackConfig = merge(baseWebpackConfig, {
|
||||||
|
entry: {
|
||||||
|
'index': './examples/main.js',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: utils.styleLoaders({
|
||||||
|
sourceMap: config.example.productionSourceMap,
|
||||||
|
extract: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
devtool: config.example.productionSourceMap ? '#source-map' : false,
|
||||||
|
output: {
|
||||||
|
path: config.example.assetsRoot,
|
||||||
|
filename: utils.assetsPath('js/[name].[chunkhash:7].js'),
|
||||||
|
chunkFilename: utils.assetsPath('js/[name].[chunkhash:7].js'),
|
||||||
|
publicPath: config.example.assetsPublicPath
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new ProgressBarPlugin({
|
||||||
|
format: ' BUILD EXAMPLES [:bar] :percent (:elapsed seconds)',
|
||||||
|
clear: false
|
||||||
|
}),
|
||||||
|
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': env
|
||||||
|
}),
|
||||||
|
// UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
compress: {
|
||||||
|
warnings: false
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
ascii_only: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// extract css into its own file
|
||||||
|
new ExtractTextPlugin({
|
||||||
|
filename: utils.assetsPath('css/[name].[contenthash:7].css')
|
||||||
|
}),
|
||||||
|
// Compress extracted CSS. We are using this plugin so that possible
|
||||||
|
// duplicated CSS from different components can be deduped.
|
||||||
|
new OptimizeCSSPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
safe: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// generate dist index.html with correct asset hash for caching.
|
||||||
|
// you can customize output by editing /index.html
|
||||||
|
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: config.example.index,
|
||||||
|
template: './examples/index.html',
|
||||||
|
inject: true,
|
||||||
|
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||||
|
chunksSortMode: 'dependency'
|
||||||
|
}),
|
||||||
|
// keep module.id stable when vender modules does not change
|
||||||
|
new webpack.HashedModuleIdsPlugin(),
|
||||||
|
// split vendor js into its own file
|
||||||
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
|
name: 'vendor',
|
||||||
|
minChunks: function (module) {
|
||||||
|
// any required modules inside node_modules are extracted to vendor
|
||||||
|
return (
|
||||||
|
module.resource &&
|
||||||
|
/\.js$/.test(module.resource) &&
|
||||||
|
module.resource.indexOf(
|
||||||
|
path.join(__dirname, '../../node_modules')
|
||||||
|
) === 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// extract webpack runtime and module manifest to its own file in order to
|
||||||
|
// prevent vendor hash from being updated whenever app bundle is updated
|
||||||
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
|
name: 'manifest',
|
||||||
|
chunks: ['vendor']
|
||||||
|
}),
|
||||||
|
// copy custom static assets
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, '../static'),
|
||||||
|
to: config.example.assetsSubDirectory,
|
||||||
|
ignore: ['.*']
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
options: {
|
||||||
|
stylus: {
|
||||||
|
use: [poststylus([
|
||||||
|
pxtorem({
|
||||||
|
rootValue: 100,
|
||||||
|
propWhiteList: [],
|
||||||
|
})
|
||||||
|
])]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (config.example.productionGzip) {
|
||||||
|
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||||
|
|
||||||
|
webpackConfig.plugins.push(
|
||||||
|
new CompressionWebpackPlugin({
|
||||||
|
asset: '[path].gz[query]',
|
||||||
|
algorithm: 'gzip',
|
||||||
|
test: new RegExp(
|
||||||
|
'\\.(' +
|
||||||
|
config.example.productionGzipExtensions.join('|') +
|
||||||
|
')$'
|
||||||
|
),
|
||||||
|
threshold: 10240,
|
||||||
|
minRatio: 0.8
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.example.bundleAnalyzerReport) {
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||||
|
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = webpackConfig
|
|
@ -0,0 +1,32 @@
|
||||||
|
'use strict'
|
||||||
|
// This is the webpack config used for unit tests.
|
||||||
|
|
||||||
|
const utils = require('./utils')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const merge = require('webpack-merge')
|
||||||
|
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||||
|
const baseWebpackConfig = require('./webpack.base.conf')
|
||||||
|
|
||||||
|
const webpackConfig = merge(baseWebpackConfig, {
|
||||||
|
// use inline sourcemap for karma-sourcemap-loader
|
||||||
|
module: {
|
||||||
|
rules: utils.styleLoaders()
|
||||||
|
},
|
||||||
|
devtool: '#cheap-module-eval-source-map',
|
||||||
|
plugins: [
|
||||||
|
new ProgressBarPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': require('../../config/test.env')
|
||||||
|
}),
|
||||||
|
// extract css into its own file
|
||||||
|
new ExtractTextPlugin({
|
||||||
|
filename: utils.assetsPath('[name].css')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// no need for app entry during tests
|
||||||
|
delete webpackConfig.entry
|
||||||
|
|
||||||
|
module.exports = webpackConfig
|
|
@ -0,0 +1,7 @@
|
||||||
|
body
|
||||||
|
font-family font-family-normal
|
||||||
|
-webkit-tap-highlight-color transparent
|
||||||
|
-webkit-font-smoothing antialiased
|
||||||
|
-moz-osx-font-smoothing grayscale
|
||||||
|
ol, li
|
||||||
|
list-style none
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M366.08 196.096c5.632 5.632 5.632 14.336 0 19.968l-99.84 99.84c-5.632 5.632-14.848 5.632-19.968 0l-100.352-99.84c-5.632-5.632-5.632-14.336 0-19.968s14.336-5.632 19.968 0L256 286.208l90.112-90.112c5.632-5.632 14.336-5.632 19.968 0z"/></svg>
|
After Width: | Height: | Size: 343 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M315.904 366.08c-5.632 5.632-14.336 5.632-19.968 0l-99.84-100.352c-5.632-5.12-5.632-14.336 0-19.968l99.84-99.84c5.632-5.632 14.336-5.632 19.968 0s5.632 14.336 0 19.968L225.792 256l90.112 90.112c5.632 5.632 5.632 14.336 0 19.968z"/></svg>
|
After Width: | Height: | Size: 341 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M196.096 145.92c5.632-5.632 14.336-5.632 19.968 0l99.84 99.84c5.632 5.632 5.632 14.848 0 19.968l-99.84 100.352c-5.632 5.632-14.336 5.632-19.968 0s-5.632-14.336 0-19.968L286.208 256l-90.112-90.112c-5.632-5.632-5.632-14.336 0-19.968z"/></svg>
|
After Width: | Height: | Size: 344 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M145.92 315.904c-5.632-5.632-5.632-14.336 0-19.968l100.352-99.84c5.12-5.632 14.336-5.632 19.968 0l99.84 99.84c5.632 5.632 5.632 14.336 0 19.968s-14.336 5.632-19.968 0L256 225.792l-90.112 90.112c-5.632 5.632-14.336 5.632-19.968 0z"/></svg>
|
After Width: | Height: | Size: 342 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M256 496C123.449 496 16 388.551 16 256S123.449 16 256 16s240 107.449 240 240-107.449 240-240 240zm-23.441-375l7.031 165H271l8.441-165h-46.879zm44.692 218.76c-5.921-5.809-13.069-8.719-21.439-8.719-8.381 0-15.461 2.91-21.24 8.719-5.779 5.831-8.681 12.881-8.681 21.18 0 9.499 3.03 16.89 9.079 22.17 6.049 5.291 13.129 7.931 21.24 7.931 7.969 0 14.951-2.681 20.94-8.029 5.981-5.34 8.97-12.701 8.97-22.069 0-8.299-2.959-15.349-8.869-21.18z"/></svg>
|
After Width: | Height: | Size: 547 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M256 29.696C131.072 29.696 29.696 131.072 29.696 256S131.072 482.304 256 482.304 482.304 380.928 482.304 256 380.928 29.696 256 29.696zm90.112 296.448l-19.968 19.968L256 275.968l-70.144 70.144-19.968-19.968L236.032 256l-70.144-70.144 19.968-19.968L256 236.032l70.144-70.144 19.968 19.968L275.968 256l70.144 70.144z"/></svg>
|
After Width: | Height: | Size: 427 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M256 29.696C131.072 29.696 29.696 131.072 29.696 256S131.072 482.304 256 482.304 482.304 380.928 482.304 256 380.928 29.696 256 29.696zm-22.528 304.64l.512.512-19.968 19.968L128 268.8l19.968-19.968 65.536 65.536 145.92-145.92 19.968 19.968-145.92 145.92z"/></svg>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path fill="none" stroke="#ccc" stroke-width="24.381" d="M467.81 256c0 116.98-94.83 211.81-211.81 211.81S44.19 372.98 44.19 256 139.02 44.19 256 44.19 467.81 139.02 467.81 256z"/></svg>
|
After Width: | Height: | Size: 280 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M111.104 91.136L256 236.032 400.896 91.136l19.968 19.968L275.968 256l144.896 144.896-19.968 19.968L256 275.968 111.104 420.864l-19.968-19.968L236.032 256 91.136 111.104l19.968-19.968z"/></svg>
|
After Width: | Height: | Size: 296 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M241.778 270.222v128c0 7.854 6.368 14.222 14.222 14.222s14.222-6.368 14.222-14.222v-128h128c7.854 0 14.222-6.368 14.222-14.222s-6.368-14.222-14.222-14.222h-128v-128c0-7.855-6.368-14.222-14.222-14.222s-14.222 6.367-14.222 14.222v128h-128c-7.855 0-14.222 6.368-14.222 14.222s6.367 14.222 14.222 14.222h128z"/><path d="M0 256C0 114.615 114.615 0 256 0s256 114.615 256 256-114.615 256-256 256S0 397.385 0 256zm28.445 0c0 125.675 101.88 227.555 227.555 227.555S483.555 381.675 483.555 256c0-125.675-101.88-227.555-227.555-227.555S28.445 130.325 28.445 256z"/></svg>
|
After Width: | Height: | Size: 664 B |
After Width: | Height: | Size: 543 B |
After Width: | Height: | Size: 947 B |
After Width: | Height: | Size: 493 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="670" height="512" viewBox="0 0 670 512"><title/><path d="M222.793 371.595L55.698 204.5-.001 260.198l222.793 222.793L640.529 65.254 584.831 9.555 222.793 371.593z"/><path d="M55.699 232.35L27.85 260.199l194.944 194.944L612.682 65.255l-27.849-27.849-362.038 362.038L55.7 232.349z"/></svg>
|
After Width: | Height: | Size: 333 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg class="lds-spinner" width="200" height="200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background:0 0"><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(30 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(60 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(90 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(120 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(150 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(180 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(210 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(240 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(270 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(300 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(330 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite"/></rect></svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,310 @@
|
||||||
|
/*
|
||||||
|
* components
|
||||||
|
*/
|
||||||
|
|
||||||
|
// button
|
||||||
|
button-primary-fill = color-primary
|
||||||
|
button-primary-fill-disabled = color-bg-disabled
|
||||||
|
button-primary-fill-tap = color-bg-tap
|
||||||
|
button-primary-width = 100%
|
||||||
|
button-primary-height = 100px
|
||||||
|
button-primary-font-size = 32px
|
||||||
|
|
||||||
|
button-ghost-fill = color-bg-base
|
||||||
|
button-ghost-fill-tap = color-bg-tap
|
||||||
|
button-ghost-primary-fill-tap = color-bg-tap-hightlight
|
||||||
|
button-ghost-width = 160px
|
||||||
|
button-ghost-height = 60px
|
||||||
|
button-ghost-width-sm = 130px
|
||||||
|
button-ghost-height-sm = 50px
|
||||||
|
button-ghost-font-size = 24px
|
||||||
|
button-ghost-color = color-border-element
|
||||||
|
button-ghost-primary-color = color-primary
|
||||||
|
|
||||||
|
button-link-fill = color-bg-base
|
||||||
|
button-link-fill-tap = color-bg-tap
|
||||||
|
button-link-width = 100%
|
||||||
|
button-link-height = 100px
|
||||||
|
button-link-color = color-primary-tap
|
||||||
|
|
||||||
|
// icon
|
||||||
|
icon-size-xs = 20px
|
||||||
|
icon-size-sm = 24px
|
||||||
|
icon-size-md = 32px
|
||||||
|
icon-size-lg = 42px
|
||||||
|
|
||||||
|
// action-bar
|
||||||
|
action-bar-width = 100%
|
||||||
|
action-bar-height = 100px
|
||||||
|
action-bar-button-font-size = 32px
|
||||||
|
action-bar-text-font-size = 36px
|
||||||
|
action-bar-button-color = color-text-base
|
||||||
|
action-bar-button-color-hightlight = color-text-base-inverse
|
||||||
|
action-bar-text-color = color-primary
|
||||||
|
action-bar-button-fill = color-bg-base
|
||||||
|
action-bar-button-fill-hightlight = color-primary
|
||||||
|
action-bar-shadow = shadow-top
|
||||||
|
action-bar-zindex = 100
|
||||||
|
|
||||||
|
// notice-bar
|
||||||
|
notice-bar-fill = #4A4C5B
|
||||||
|
notice-bar-color = color-text-base-inverse
|
||||||
|
notice-bar-zindex = 1300
|
||||||
|
|
||||||
|
// stepper
|
||||||
|
stepper-fill = color-primary-background
|
||||||
|
stepper-disabled-opacity = opacity-disabled
|
||||||
|
stepper-color = color-text-base
|
||||||
|
stepper-height = 50px
|
||||||
|
stepper-width-button = 50px
|
||||||
|
stepper-width-input = 60px
|
||||||
|
stepper-radius-button = 0 0 0 0
|
||||||
|
stepper-radius-input = 0 0 0 0
|
||||||
|
|
||||||
|
// steps
|
||||||
|
steps-color = color-text-disabled
|
||||||
|
steps-color-active = color-primary
|
||||||
|
steps-border = dotted 2px steps-color
|
||||||
|
steps-border-active = solid 2px steps-color
|
||||||
|
steps-size = 12px
|
||||||
|
steps-size-active = 32px
|
||||||
|
|
||||||
|
// tab
|
||||||
|
tab-color = color-primary-tap
|
||||||
|
tab-font-size = 28px
|
||||||
|
tab-height = 80px
|
||||||
|
tab-ink-bar-height = 3px
|
||||||
|
tab-zindex = 101
|
||||||
|
|
||||||
|
// field
|
||||||
|
field-padding = h-gap-lg
|
||||||
|
field-padding-h = 32px
|
||||||
|
field-padding-v = 29px
|
||||||
|
field-title-font-size = 28px
|
||||||
|
field-title-weight = font-weight-medium = 500
|
||||||
|
field-title-color = #333
|
||||||
|
field-title-font-weight = font-weight-medium
|
||||||
|
field-title-margin = 26px
|
||||||
|
field-item-height = 100px
|
||||||
|
field-item-padding-v = 22px
|
||||||
|
field-item-bg-color = color-bg-base
|
||||||
|
field-item-color = color-text-base
|
||||||
|
field-item-color-check = color-text-minor
|
||||||
|
field-item-color-action = color-text-link
|
||||||
|
field-item-font-size = 28px
|
||||||
|
field-item-font-size-check = 24px
|
||||||
|
field-item-icon-color = #CCC
|
||||||
|
field-item-border-color = #E6E6E6
|
||||||
|
field-item-color-disabled = opacity-disabled
|
||||||
|
|
||||||
|
// input-item
|
||||||
|
input-item-height = 100px
|
||||||
|
input-item-title-width = 170px
|
||||||
|
input-item-title-gap = 22px
|
||||||
|
input-item-font-size = 28px
|
||||||
|
input-item-title-latent-font-size = 26px
|
||||||
|
input-item-font-size-large = 42px
|
||||||
|
input-item-font-size-error = 22px
|
||||||
|
input-item-font-weight = font-weight-normal
|
||||||
|
input-item-color = color-text-base
|
||||||
|
input-item-title-latent-color = #666
|
||||||
|
input-item-color-disabled = opacity-disabled
|
||||||
|
input-item-color-error = #FF525D
|
||||||
|
input-item-placeholder = color-text-placeholder
|
||||||
|
input-item-placeholder-hightlight = color-primary
|
||||||
|
input-item-icon = color-text-placeholder // delete icon
|
||||||
|
|
||||||
|
// radio
|
||||||
|
radio-fill = color-primary-tap
|
||||||
|
|
||||||
|
// switch
|
||||||
|
switch-fill = color-primary
|
||||||
|
switch-fill-inverse = color-bg-disabled
|
||||||
|
switch-handle-color = #FFF
|
||||||
|
switch-item-color-disabled = opacity-disabled
|
||||||
|
|
||||||
|
// agree
|
||||||
|
agree-fill = color-primary
|
||||||
|
agree-fill-inverse = color-bg-disabled
|
||||||
|
agree-size-sm = 32px
|
||||||
|
agree-size-lg = 44px
|
||||||
|
|
||||||
|
// action-sheet
|
||||||
|
action-sheet-height = 120px
|
||||||
|
action-sheet-font-size = 30px
|
||||||
|
action-sheet-zindex = 1101
|
||||||
|
|
||||||
|
// picker
|
||||||
|
picker-font-size = 30px
|
||||||
|
picker-disabled-opacity = .2
|
||||||
|
picker-color = color-text-base
|
||||||
|
picker-zindex = 1100
|
||||||
|
|
||||||
|
// selector
|
||||||
|
selector-height= 100px
|
||||||
|
selector-disabled-opacity = .2
|
||||||
|
selector-font-size = 30px
|
||||||
|
selector-color = color-text-base
|
||||||
|
selector-zindex = 1102
|
||||||
|
|
||||||
|
// dialog
|
||||||
|
dialog-width = 534px
|
||||||
|
dialog-radius = 0 0 0 0
|
||||||
|
dialog-title-font-size = 32px
|
||||||
|
dialog-text-font-size = 28px
|
||||||
|
dialog-action-height = 100px
|
||||||
|
dialog-action-font-size = 32px
|
||||||
|
dialog-icon-size = 100px
|
||||||
|
dialog-icon-fill = color-text-caption
|
||||||
|
dialog-zindex = 1402
|
||||||
|
|
||||||
|
// toast
|
||||||
|
toast-fill = rgba(0, 0, 0, .8)
|
||||||
|
toast-font-size = 28px
|
||||||
|
toast-color = #ccc
|
||||||
|
toast-zindex = 1401
|
||||||
|
|
||||||
|
// tip
|
||||||
|
tip-fill = rgba(74, 76, 91, 0.8)
|
||||||
|
tip-font-size = 24px
|
||||||
|
tip-color = #fff
|
||||||
|
tip-zindex = 1300
|
||||||
|
|
||||||
|
// captcha
|
||||||
|
captcha-zindex = 1400
|
||||||
|
captcha-keyboard-zindex = 1403
|
||||||
|
captcha-content-offset-top = 60px
|
||||||
|
|
||||||
|
// codebox
|
||||||
|
codebox-font-size = font-body-normal
|
||||||
|
codebox-width = 60px
|
||||||
|
codebox-gutter = 10px
|
||||||
|
codebox-border-color = color-border-base
|
||||||
|
codebox-border-active-color = color-primary
|
||||||
|
codebox-blink-color = color-primary
|
||||||
|
codebox-input-height = 68px
|
||||||
|
codebox-input-padding = 16px 32px
|
||||||
|
codebox-input-font-size = 28px
|
||||||
|
codebox-input-border-color = color-border-base
|
||||||
|
codebox-dot-color = #000
|
||||||
|
|
||||||
|
// chart
|
||||||
|
chart-line-color = #ccc
|
||||||
|
chart-path-color = #fa8919
|
||||||
|
chart-text-color = #666
|
||||||
|
chart-label-size = 22px
|
||||||
|
chart-value-size = 20px
|
||||||
|
|
||||||
|
// popup
|
||||||
|
popup-title-bar-height = 110px
|
||||||
|
popup-title-bar-font-size-button = 28px
|
||||||
|
popup-title-bar-font-size-title = 36px
|
||||||
|
popup-zindex = 1000
|
||||||
|
|
||||||
|
// drop-menu
|
||||||
|
drop-menu-height = 82px
|
||||||
|
drop-menu-zindex = 1200
|
||||||
|
drop-menu-color = color-text-link
|
||||||
|
drop-menu-font-size = font-body-normal
|
||||||
|
|
||||||
|
// number-keyboard
|
||||||
|
number-keyboard-width = 100%
|
||||||
|
number-keyboard-height = 428px
|
||||||
|
number-keyboard-key-height = 107px
|
||||||
|
number-keyboard-key-bg = #ebebeb
|
||||||
|
number-keyboard-key-bg-tap = #f0f0f0
|
||||||
|
number-keyboard-key-confirm-bg = color-primary
|
||||||
|
number-keyboard-key-confirm-bg-tap = #DD7F49
|
||||||
|
number-keyboard-key-font-size = 48px
|
||||||
|
number-keyboard-key-color = color-text-minor
|
||||||
|
number-keyboard-key-color-simple = #000
|
||||||
|
number-keyboard-zindex = 1403
|
||||||
|
|
||||||
|
//tab-picker
|
||||||
|
tab-picker-font-size = 28px
|
||||||
|
tab-picker-color = color-text-base
|
||||||
|
tab-picker-hignlight-color = color-primary
|
||||||
|
tab-picker-min-height = 100px
|
||||||
|
|
||||||
|
// date-picker
|
||||||
|
date-picker-font-size = 28px
|
||||||
|
date-time-picker-font-size = 24px
|
||||||
|
|
||||||
|
/*
|
||||||
|
* global color
|
||||||
|
*/
|
||||||
|
|
||||||
|
// brand color
|
||||||
|
color-primary = #fc9153 // 1st main color for buttons & hightlight text which is not clickable
|
||||||
|
color-primary-tap = #3ca0e6 // 2nd main color for links and selected element
|
||||||
|
color-primary-background = #f3f4f5 // 3rd main color for background
|
||||||
|
|
||||||
|
// text color
|
||||||
|
color-text-base = #333 // default text color
|
||||||
|
color-text-base-inverse = #fff // default inverse text color
|
||||||
|
color-text-minor = #666 // auxiliary text color
|
||||||
|
color-text-caption = #999 // subtitle and describe text color
|
||||||
|
color-text-disabled = #ccc // input placeholder
|
||||||
|
color-text-placeholder = #ccc // input placeholder
|
||||||
|
color-text-hightlight = color-primary // hight text color
|
||||||
|
color-text-link = color-primary-tap // link text color
|
||||||
|
|
||||||
|
// border color
|
||||||
|
color-border-base = #d9d9d9 // defalut gap color of items
|
||||||
|
color-border-minor = #ebebeb // gap color of items
|
||||||
|
color-border-element = #999 // border color of element such as button
|
||||||
|
|
||||||
|
// background color
|
||||||
|
color-bg-base = #fff // default background color
|
||||||
|
color-bg-disabled = #CCC // background color for disabeld element
|
||||||
|
color-bg-mask = rgba(0, 0, 0, .4) // background color for mask layer
|
||||||
|
color-bg-tap = rgba(0, 0, 0, .08) // background color for element click state
|
||||||
|
color-bg-tap-hightlight = rgba(252, 145, 83, .08) // background color for hightlight element click state
|
||||||
|
|
||||||
|
// opacity
|
||||||
|
opacity-disabled = .4 // opacity of disabled button, switch, agree
|
||||||
|
|
||||||
|
/*
|
||||||
|
* global size
|
||||||
|
*/
|
||||||
|
|
||||||
|
// text size
|
||||||
|
font-heading-large = 42px
|
||||||
|
font-heading-medium = 36px
|
||||||
|
font-heading-normal = 32px
|
||||||
|
font-body-large = 30px
|
||||||
|
font-body-normal = 28px
|
||||||
|
font-minor-large = 24px
|
||||||
|
font-minor-normal = 20px
|
||||||
|
|
||||||
|
font-weight-normal = 400
|
||||||
|
font-weight-medium = 500
|
||||||
|
font-weight-bold = 600
|
||||||
|
|
||||||
|
// radius size
|
||||||
|
radius-normal = 4px
|
||||||
|
radius-circle = 50%
|
||||||
|
|
||||||
|
// border size
|
||||||
|
border-width-base = 2px
|
||||||
|
|
||||||
|
// gap size
|
||||||
|
h-gap-sm = 12px
|
||||||
|
h-gap-md = 20px
|
||||||
|
h-gap-lg = 32px
|
||||||
|
v-gap-sm = 12px
|
||||||
|
v-gap-md = 20px
|
||||||
|
v-gap-lg = 32px
|
||||||
|
|
||||||
|
/*
|
||||||
|
* global other
|
||||||
|
*/
|
||||||
|
|
||||||
|
// box shadow
|
||||||
|
shadow-bottom = 0 2px 4px rgba(0, 0, 0, .12)
|
||||||
|
shadow-top = 0 -2px 4px rgba(0, 0, 0, .12)
|
||||||
|
|
||||||
|
// animate
|
||||||
|
ease-in-out-quint = cubic-bezier(.86, 0, .07, 1)
|
||||||
|
|
||||||
|
font-family-normal = "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif
|
|
@ -0,0 +1,105 @@
|
||||||
|
absolute-pos(t = 0, r = 0, b = 0, l = 0)
|
||||||
|
top t
|
||||||
|
right r
|
||||||
|
bottom b
|
||||||
|
left l
|
||||||
|
fixed-pos(t = 0, r = 0, b = 0, l = 0)
|
||||||
|
absolute-pos(t, r, b, l)
|
||||||
|
|
||||||
|
clearfix()
|
||||||
|
&:after
|
||||||
|
content ""
|
||||||
|
display table
|
||||||
|
clear both
|
||||||
|
// normalize()
|
||||||
|
// margin 0
|
||||||
|
// padding 0
|
||||||
|
// list-style none
|
||||||
|
|
||||||
|
word-break()
|
||||||
|
word-break break-all
|
||||||
|
word-wrap break-word
|
||||||
|
word-ellipsis()
|
||||||
|
white-space nowrap
|
||||||
|
overflow hidden
|
||||||
|
text-overflow ellipsis
|
||||||
|
|
||||||
|
hairline-common(direction, color)
|
||||||
|
content ''
|
||||||
|
position absolute
|
||||||
|
z-index 2
|
||||||
|
background-color color
|
||||||
|
transform-origin 100% 50%
|
||||||
|
if direction == top
|
||||||
|
transform scaleY(0.5) translateY(-100%)
|
||||||
|
@media (min-resolution: 3dppx)
|
||||||
|
transform scaleY(0.33) translateY(-100%)
|
||||||
|
else if direction == bottom
|
||||||
|
transform scaleY(0.5) translateY(100%)
|
||||||
|
@media (min-resolution: 3dppx)
|
||||||
|
transform scaleY(0.33) translateY(100%)
|
||||||
|
else if direction == left
|
||||||
|
transform scaleX(0.5) translateX(-100%)
|
||||||
|
@media (min-resolution: 3dppx)
|
||||||
|
transform scaleX(0.33) translateX(-100%)
|
||||||
|
else if direction == right
|
||||||
|
transform scaleX(0.5) translateX(100%)
|
||||||
|
@media (min-resolution: 3dppx)
|
||||||
|
transform scaleX(0.33) translateX(100%)
|
||||||
|
|
||||||
|
hairline(direction = all, color = color-line-1, radius = false)
|
||||||
|
position relative
|
||||||
|
if direction == top
|
||||||
|
&::after
|
||||||
|
hairline-common(direction, color)
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
height border-width-base
|
||||||
|
|
||||||
|
else if direction == bottom
|
||||||
|
&::before
|
||||||
|
hairline-common(direction, color)
|
||||||
|
bottom 0
|
||||||
|
left 0
|
||||||
|
width 100%
|
||||||
|
height border-width-base
|
||||||
|
|
||||||
|
else if direction == left
|
||||||
|
&::after
|
||||||
|
hairline-common(direction, color)
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width border-width-base
|
||||||
|
height 100%
|
||||||
|
|
||||||
|
else if direction == right
|
||||||
|
&::before
|
||||||
|
hairline-common(direction, color)
|
||||||
|
top 0
|
||||||
|
right 0
|
||||||
|
width border-width-base
|
||||||
|
height 100%
|
||||||
|
|
||||||
|
else
|
||||||
|
&::after
|
||||||
|
content ''
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
width 200%
|
||||||
|
height 200%
|
||||||
|
border solid border-width-base color
|
||||||
|
box-sizing border-box
|
||||||
|
transform-origin 0 0
|
||||||
|
transform scale(0.5)
|
||||||
|
z-index 2
|
||||||
|
if radius
|
||||||
|
border-radius radius-normal * 2
|
||||||
|
|
||||||
|
svg-background(svg)
|
||||||
|
background-image url(svg)
|
||||||
|
|
||||||
|
vertical-height(height)
|
||||||
|
height height
|
||||||
|
line-height height
|
|
@ -0,0 +1,204 @@
|
||||||
|
const Animate = (global => {
|
||||||
|
const time =
|
||||||
|
Date.now ||
|
||||||
|
(() => {
|
||||||
|
return +new Date()
|
||||||
|
})
|
||||||
|
const desiredFrames = 60
|
||||||
|
const millisecondsPerSecond = 1000
|
||||||
|
|
||||||
|
let running = {}
|
||||||
|
let counter = 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* A requestAnimationFrame wrapper / polyfill.
|
||||||
|
*
|
||||||
|
* @param callback {Function} The callback to be invoked before the next repaint.
|
||||||
|
* @param root {HTMLElement} The root element for the repaint
|
||||||
|
*/
|
||||||
|
requestAnimationFrame: (() => {
|
||||||
|
// Check for request animation Frame support
|
||||||
|
const requestFrame =
|
||||||
|
global.requestAnimationFrame ||
|
||||||
|
global.webkitRequestAnimationFrame ||
|
||||||
|
global.mozRequestAnimationFrame ||
|
||||||
|
global.oRequestAnimationFrame
|
||||||
|
let isNative = !!requestFrame
|
||||||
|
|
||||||
|
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
|
||||||
|
isNative = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNative) {
|
||||||
|
return (callback, root) => {
|
||||||
|
requestFrame(callback, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TARGET_FPS = 60
|
||||||
|
let requests = {}
|
||||||
|
let requestCount = 0
|
||||||
|
let rafHandle = 1
|
||||||
|
let intervalHandle = null
|
||||||
|
let lastActive = +new Date()
|
||||||
|
|
||||||
|
return callback => {
|
||||||
|
const callbackHandle = rafHandle++
|
||||||
|
|
||||||
|
// Store callback
|
||||||
|
requests[callbackHandle] = callback
|
||||||
|
requestCount++
|
||||||
|
|
||||||
|
// Create timeout at first request
|
||||||
|
if (intervalHandle === null) {
|
||||||
|
intervalHandle = setInterval(() => {
|
||||||
|
const time = +new Date()
|
||||||
|
const currentRequests = requests
|
||||||
|
|
||||||
|
// Reset data structure before executing callbacks
|
||||||
|
requests = {}
|
||||||
|
requestCount = 0
|
||||||
|
|
||||||
|
for (const key in currentRequests) {
|
||||||
|
if (currentRequests.hasOwnProperty(key)) {
|
||||||
|
currentRequests[key](time)
|
||||||
|
lastActive = time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the timeout when nothing happens for a certain
|
||||||
|
// period of time
|
||||||
|
if (time - lastActive > 2500) {
|
||||||
|
clearInterval(intervalHandle)
|
||||||
|
intervalHandle = null
|
||||||
|
}
|
||||||
|
}, 1000 / TARGET_FPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return callbackHandle
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the given animation.
|
||||||
|
*
|
||||||
|
* @param id {Integer} Unique animation ID
|
||||||
|
* @return {Boolean} Whether the animation was stopped (aka, was running before)
|
||||||
|
*/
|
||||||
|
stop(id) {
|
||||||
|
const cleared = running[id] != null
|
||||||
|
cleared && (running[id] = null)
|
||||||
|
return cleared
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given animation is still running.
|
||||||
|
*
|
||||||
|
* @param id {Integer} Unique animation ID
|
||||||
|
* @return {Boolean} Whether the animation is still running
|
||||||
|
*/
|
||||||
|
isRunning(id) {
|
||||||
|
return running[id] != null
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the animation.
|
||||||
|
*
|
||||||
|
* @param stepCallback {Function} Pointer to function which is executed on every step.
|
||||||
|
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
|
||||||
|
* @param verifyCallback {Function} Executed before every animation step.
|
||||||
|
* Signature of the method should be `function() { return continueWithAnimation; }`
|
||||||
|
* @param completedCallback {Function}
|
||||||
|
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
|
||||||
|
* @param duration {Integer} Milliseconds to run the animation
|
||||||
|
* @param easingMethod {Function} Pointer to easing function
|
||||||
|
* Signature of the method should be `function(percent) { return modifiedValue; }`
|
||||||
|
* @param root {Element ? document.body} Render root, when available. Used for internal
|
||||||
|
* usage of requestAnimationFrame.
|
||||||
|
* @return {Integer} Identifier of animation. Can be used to stop it any time.
|
||||||
|
*/
|
||||||
|
start(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
|
||||||
|
const start = time()
|
||||||
|
let lastFrame = start
|
||||||
|
let percent = 0
|
||||||
|
let dropCounter = 0
|
||||||
|
const id = counter++
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
root = document.body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compacting running db automatically every few new animations
|
||||||
|
if (id % 20 === 0) {
|
||||||
|
const newRunning = {}
|
||||||
|
for (const usedId in running) {
|
||||||
|
newRunning[usedId] = true
|
||||||
|
}
|
||||||
|
running = newRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the internal step method which is called every few milliseconds
|
||||||
|
const step = virtual => {
|
||||||
|
// Normalize virtual value
|
||||||
|
const render = virtual !== true
|
||||||
|
|
||||||
|
// Get current time
|
||||||
|
const now = time()
|
||||||
|
|
||||||
|
// Verification is executed before next animation step
|
||||||
|
if (!running[id] || (verifyCallback && !verifyCallback(id))) {
|
||||||
|
running[id] = null
|
||||||
|
completedCallback &&
|
||||||
|
completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the current rendering to apply let's update omitted steps in memory.
|
||||||
|
// This is important to bring internal state variables up-to-date with progress in time.
|
||||||
|
if (render) {
|
||||||
|
const droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1
|
||||||
|
for (let j = 0; j < Math.min(droppedFrames, 4); j++) {
|
||||||
|
step(true)
|
||||||
|
dropCounter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute percent value
|
||||||
|
if (duration) {
|
||||||
|
percent = (now - start) / duration
|
||||||
|
if (percent > 1) {
|
||||||
|
percent = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute step callback, then...
|
||||||
|
let value = easingMethod ? easingMethod(percent) : percent
|
||||||
|
value = isNaN(value) ? 0 : value
|
||||||
|
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
|
||||||
|
running[id] = null
|
||||||
|
completedCallback &&
|
||||||
|
completedCallback(
|
||||||
|
desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond),
|
||||||
|
id,
|
||||||
|
percent === 1 || duration == null,
|
||||||
|
)
|
||||||
|
} else if (render) {
|
||||||
|
lastFrame = now
|
||||||
|
this.requestAnimationFrame(step, root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as running
|
||||||
|
running[id] = true
|
||||||
|
|
||||||
|
// Init first step
|
||||||
|
this.requestAnimationFrame(step, root)
|
||||||
|
|
||||||
|
// Return unique animation ID
|
||||||
|
return id
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})(window)
|
||||||
|
|
||||||
|
export default Animate
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {isProd} from './env'
|
||||||
|
|
||||||
|
export const warn = (msg, fn = 'error') => {
|
||||||
|
!isProd && console[fn](`[Mand-Mobile]: ${msg}`)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Development environment
|
||||||
|
export const isProd = process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
|
// Browser environment sniffing
|
||||||
|
export const inBrowser = typeof window !== 'undefined'
|
||||||
|
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
|
||||||
|
export const isAndroid = UA && UA.indexOf('android') > 0
|
||||||
|
export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA)
|
|
@ -0,0 +1,76 @@
|
||||||
|
export function formatValueByGapRule(gapRule, value, gap = ' ', range, isAdd = 1) {
|
||||||
|
const arr = value ? value.split('') : []
|
||||||
|
let showValue = ''
|
||||||
|
const rule = []
|
||||||
|
gapRule.split('|').some((n, j) => {
|
||||||
|
rule[j] = +n + (rule[j - 1] ? +rule[j - 1] : 0)
|
||||||
|
})
|
||||||
|
let j = 0
|
||||||
|
arr.some((n, i) => {
|
||||||
|
// Remove the excess part
|
||||||
|
if (i > rule[rule.length - 1] - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i > 0 && i === rule[j]) {
|
||||||
|
showValue = showValue + gap + n
|
||||||
|
j++
|
||||||
|
} else {
|
||||||
|
showValue = showValue + '' + n
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let adapt = 0
|
||||||
|
rule.some((n, j) => {
|
||||||
|
if (range === +n + 1 + j) {
|
||||||
|
adapt = 1 * isAdd
|
||||||
|
}
|
||||||
|
})
|
||||||
|
range = typeof range !== 'undefined' ? (range === 0 ? 0 : range + adapt) : showValue.length
|
||||||
|
return {value: showValue, range: range}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatValueByGapStep(step, value, gap = ' ', direction = 'right', range, isAdd = 1, oldValue = '') {
|
||||||
|
if (value.length === 0) {
|
||||||
|
return {value, range}
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr = value && value.split('')
|
||||||
|
let _range = range
|
||||||
|
let showValue = ''
|
||||||
|
|
||||||
|
if (direction === 'right') {
|
||||||
|
for (let j = arr.length - 1, k = 0; j >= 0; j--, k++) {
|
||||||
|
const m = arr[j]
|
||||||
|
showValue = k > 0 && k % step === 0 ? m + gap + showValue : m + '' + showValue
|
||||||
|
}
|
||||||
|
if (isAdd === 1) {
|
||||||
|
// 在添加的情况下,如果添加前字符串的长度减去新的字符串的长度为2,说明多了一个间隔符,需要调整range
|
||||||
|
if (oldValue.length - showValue.length === -2) {
|
||||||
|
_range = range + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 在删除情况下,如果删除前字符串的长度减去新的字符串的长度为2,说明少了一个间隔符,需要调整range
|
||||||
|
if (oldValue.length - showValue.length === 2) {
|
||||||
|
_range = range - 1
|
||||||
|
}
|
||||||
|
// 删除到最开始,range 保持 0
|
||||||
|
if (_range <= 0) {
|
||||||
|
_range = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arr.some((n, i) => {
|
||||||
|
showValue = i > 0 && i % step === 0 ? showValue + gap + n : showValue + '' + n
|
||||||
|
})
|
||||||
|
const adapt = range % (step + 1) === 0 ? 1 * isAdd : 0
|
||||||
|
_range = typeof range !== 'undefined' ? (range === 0 ? 0 : range + adapt) : showValue.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return {value: showValue, range: _range}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trimValue(value, gap = ' ') {
|
||||||
|
value = typeof value === 'undefined' ? '' : value
|
||||||
|
const reg = new RegExp(gap, 'g')
|
||||||
|
value = value.toString().replace(reg, '')
|
||||||
|
return value
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './debug'
|
||||||
|
export * from './env'
|
||||||
|
export * from './store'
|
||||||
|
export * from './lang'
|
|
@ -0,0 +1,71 @@
|
||||||
|
export function noop() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include external script dynamically
|
||||||
|
*/
|
||||||
|
export function requireRemoteScript(src, callback) {
|
||||||
|
const doc = document
|
||||||
|
const head = doc.head || doc.getElementsByTagName('head')[0]
|
||||||
|
|
||||||
|
let node = doc.createElement('script')
|
||||||
|
const supportOnload = 'onload' in node
|
||||||
|
const onload = function() {
|
||||||
|
node = null
|
||||||
|
typeof callback === 'function' && callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportOnload) {
|
||||||
|
node.onload = onload
|
||||||
|
} else {
|
||||||
|
node.onreadystatechange = function() {
|
||||||
|
if (/loaded|complete/.test(node.readyState)) {
|
||||||
|
onload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.async = true
|
||||||
|
node.crossOrigin = true
|
||||||
|
node.charset = 'utf-8'
|
||||||
|
node.src = src
|
||||||
|
head.appendChild(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDpr() {
|
||||||
|
const getParam = (name, str) => {
|
||||||
|
const reg = new RegExp(`(^|,)${name}=([^,]*)(,|$)`, 'i')
|
||||||
|
const r = str.match(reg)
|
||||||
|
if (r != null) {
|
||||||
|
return r[2]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewPort = document.querySelector('meta[name=viewport]')
|
||||||
|
|
||||||
|
if (!viewPort) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewPortContent = viewPort.getAttribute('content')
|
||||||
|
const initialScale = +(getParam('initial-scale', viewPortContent) || 1)
|
||||||
|
const maximumScale = +(getParam('maximum-scale', viewPortContent) || 1)
|
||||||
|
const minimumScale = +(getParam('minimum-scale', viewPortContent) || 1)
|
||||||
|
|
||||||
|
return 1 / Math.min(initialScale, maximumScale, minimumScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transform a Function to Blob Url
|
||||||
|
*/
|
||||||
|
export function functionToUrl(fn) {
|
||||||
|
const blob = new Blob([`(${fn.toString()})(null)`], {type: 'application/javascript'})
|
||||||
|
return URL.createObjectURL(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generate random id
|
||||||
|
*/
|
||||||
|
export function randomId(prefix = '', length = 8) {
|
||||||
|
return `${prefix}-${parseInt(Math.random() * 10 ** length)}`
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
export const render = (function(global) {
|
||||||
|
const docStyle = document.documentElement.style
|
||||||
|
|
||||||
|
let engine
|
||||||
|
|
||||||
|
if (global.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
|
||||||
|
engine = 'presto'
|
||||||
|
} else if ('MozAppearance' in docStyle) {
|
||||||
|
engine = 'gecko'
|
||||||
|
} else if ('WebkitAppearance' in docStyle) {
|
||||||
|
engine = 'webkit'
|
||||||
|
} else if (typeof navigator.cpuClass === 'string') {
|
||||||
|
engine = 'trident'
|
||||||
|
}
|
||||||
|
|
||||||
|
const vendorPrefix = {
|
||||||
|
trident: 'ms',
|
||||||
|
gecko: 'Moz',
|
||||||
|
webkit: 'Webkit',
|
||||||
|
presto: 'O',
|
||||||
|
}[engine]
|
||||||
|
|
||||||
|
const helperElem = document.createElement('div')
|
||||||
|
const perspectiveProperty = vendorPrefix + 'Perspective'
|
||||||
|
const transformProperty = vendorPrefix + 'Transform'
|
||||||
|
|
||||||
|
if (helperElem.style[perspectiveProperty] !== undefined) {
|
||||||
|
return function(content, left, top) {
|
||||||
|
// console.log(top)
|
||||||
|
content.style[transformProperty] = `translate3d(${-left}px,${-top}px,0)`
|
||||||
|
}
|
||||||
|
} else if (helperElem.style[transformProperty] !== undefined) {
|
||||||
|
return function(content, left, top) {
|
||||||
|
content.style[transformProperty] = `translate(${-left}px,${-top}px,0)`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return function(content, left, top) {
|
||||||
|
content.style.marginLeft = left ? `${-left}px` : ''
|
||||||
|
content.style.marginTop = top ? `${-top}px` : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(window)
|
|
@ -0,0 +1,915 @@
|
||||||
|
/*
|
||||||
|
* Based on the work of: Scroller
|
||||||
|
* http://github.com/zynga/scroller
|
||||||
|
*
|
||||||
|
* Copyright 2011, Zynga Inc.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import {noop, warn, extend} from './index'
|
||||||
|
|
||||||
|
import Animate from './animate'
|
||||||
|
|
||||||
|
const members = {
|
||||||
|
_isSingleTouch: false,
|
||||||
|
_isTracking: false,
|
||||||
|
_didDecelerationComplete: false,
|
||||||
|
_isGesturing: false,
|
||||||
|
_isDragging: false,
|
||||||
|
_isDecelerating: false,
|
||||||
|
_isAnimating: false,
|
||||||
|
_clientLeft: 0,
|
||||||
|
_clientTop: 0,
|
||||||
|
_clientWidth: 0,
|
||||||
|
_clientHeight: 0,
|
||||||
|
_contentWidth: 0,
|
||||||
|
_contentHeight: 0,
|
||||||
|
_snapWidth: 100,
|
||||||
|
_snapHeight: 100,
|
||||||
|
_refreshHeight: null,
|
||||||
|
_refreshActive: false,
|
||||||
|
_refreshActivate: null,
|
||||||
|
_refreshDeactivate: null,
|
||||||
|
_refreshStart: null,
|
||||||
|
_zoomLevel: 1,
|
||||||
|
_scrollLeft: 0,
|
||||||
|
_scrollTop: 0,
|
||||||
|
_maxScrollLeft: 0,
|
||||||
|
_maxScrollTop: 0,
|
||||||
|
_scheduledLeft: 0,
|
||||||
|
_scheduledTop: 0,
|
||||||
|
_lastTouchLeft: null,
|
||||||
|
_lastTouchTop: null,
|
||||||
|
_lastTouchMove: null,
|
||||||
|
_positions: null,
|
||||||
|
_minDecelerationScrollLeft: null,
|
||||||
|
_minDecelerationScrollTop: null,
|
||||||
|
_maxDecelerationScrollLeft: null,
|
||||||
|
_maxDecelerationScrollTop: null,
|
||||||
|
_decelerationVelocityX: null,
|
||||||
|
_decelerationVelocityY: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const easeOutCubic = pos => {
|
||||||
|
return Math.pow(pos - 1, 3) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const easeInOutCubic = pos => {
|
||||||
|
if ((pos /= 0.5) < 1) {
|
||||||
|
return 0.5 * Math.pow(pos, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.5 * (Math.pow(pos - 2, 3) + 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Scroller {
|
||||||
|
constructor(callback = noop, options) {
|
||||||
|
this.options = {
|
||||||
|
scrollingX: true,
|
||||||
|
scrollingY: true,
|
||||||
|
animating: true,
|
||||||
|
animationDuration: 250,
|
||||||
|
bouncing: true,
|
||||||
|
locking: true,
|
||||||
|
paging: false,
|
||||||
|
snapping: false,
|
||||||
|
zooming: false,
|
||||||
|
minZoom: 0.5,
|
||||||
|
maxZoom: 3,
|
||||||
|
speedMultiplier: 1,
|
||||||
|
scrollingComplete: noop,
|
||||||
|
penetrationDeceleration: 0.03,
|
||||||
|
penetrationAcceleration: 0.08,
|
||||||
|
}
|
||||||
|
extend(this.options, options)
|
||||||
|
this._callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the dimensions of the client (outer) and content (inner) elements.
|
||||||
|
* Requires the available space for the outer element and the outer size of the inner element.
|
||||||
|
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
||||||
|
*
|
||||||
|
* @param clientWidth {Integer ? null} Inner width of outer element
|
||||||
|
* @param clientHeight {Integer ? null} Inner height of outer element
|
||||||
|
* @param contentWidth {Integer ? null} Outer width of inner element
|
||||||
|
* @param contentHeight {Integer ? null} Outer height of inner element
|
||||||
|
*/
|
||||||
|
setDimensions(clientWidth, clientHeight, contentWidth, contentHeight) {
|
||||||
|
// Only update values which are defined
|
||||||
|
if (clientWidth === +clientWidth) {
|
||||||
|
this._clientWidth = clientWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientHeight === +clientHeight) {
|
||||||
|
this._clientHeight = clientHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentWidth === +contentWidth) {
|
||||||
|
this._contentWidth = contentWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentHeight === +contentHeight) {
|
||||||
|
this._contentHeight = contentHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh maximums
|
||||||
|
this._computeScrollMax()
|
||||||
|
|
||||||
|
// Refresh scroll position
|
||||||
|
this.scrollTo(this._scrollLeft, this._scrollTop, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client coordinates in relation to the document.
|
||||||
|
*
|
||||||
|
* @param left {Integer ? 0} Left position of outer element
|
||||||
|
* @param top {Integer ? 0} Top position of outer element
|
||||||
|
*/
|
||||||
|
setPosition(left, top) {
|
||||||
|
this._clientLeft = left || 0
|
||||||
|
this._clientTop = top || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the snapping (when snapping is active)
|
||||||
|
*
|
||||||
|
* @param width {Integer} Snapping width
|
||||||
|
* @param height {Integer} Snapping height
|
||||||
|
*/
|
||||||
|
setSnapSize(width, height) {
|
||||||
|
this._snapWidth = width
|
||||||
|
this._snapHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scroll position and zooming values
|
||||||
|
*
|
||||||
|
* @return {Map} `left` and `top` scroll position and `zoom` level
|
||||||
|
*/
|
||||||
|
getValues() {
|
||||||
|
return {
|
||||||
|
left: this._scrollLeft,
|
||||||
|
top: this._scrollTop,
|
||||||
|
zoom: this._zoomLevel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum scroll values
|
||||||
|
*
|
||||||
|
* @return {Map} `left` and `top` maximum scroll values
|
||||||
|
*/
|
||||||
|
getScrollMax() {
|
||||||
|
return {
|
||||||
|
left: this._maxScrollLeft,
|
||||||
|
top: this._maxScrollTop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
|
||||||
|
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
|
||||||
|
* the official Twitter client.
|
||||||
|
*
|
||||||
|
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
|
||||||
|
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
|
||||||
|
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
|
||||||
|
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
|
||||||
|
*/
|
||||||
|
activatePullToRefresh(height, activateCallback, deactivateCallback, startCallback) {
|
||||||
|
this._refreshHeight = height
|
||||||
|
this._refreshActivate = activateCallback
|
||||||
|
this._refreshDeactivate = deactivateCallback
|
||||||
|
this._refreshStart = startCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts pull-to-refresh manually.
|
||||||
|
*/
|
||||||
|
triggerPullToRefresh() {
|
||||||
|
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
||||||
|
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
||||||
|
this._publish(this._scrollLeft, -this._refreshHeight, this._zoomLevel, true)
|
||||||
|
|
||||||
|
if (this._refreshStart) {
|
||||||
|
this._refreshStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signalizes that pull-to-refresh is finished.
|
||||||
|
*/
|
||||||
|
finishPullToRefresh() {
|
||||||
|
this._refreshActive = false
|
||||||
|
|
||||||
|
if (this._refreshDeactivate) {
|
||||||
|
this._refreshDeactivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollTo(this._scrollLeft, this._scrollTop, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the given position. Respect limitations and snapping automatically.
|
||||||
|
*
|
||||||
|
* @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code>
|
||||||
|
* @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code>
|
||||||
|
* @param animate {Boolean?false} Whether the scrolling should happen using an animation
|
||||||
|
* @param zoom {Number?null} Zoom level to go to
|
||||||
|
*/
|
||||||
|
scrollTo(left, top, animate, zoom = 1) {
|
||||||
|
// Stop deceleration
|
||||||
|
if (this._isDecelerating) {
|
||||||
|
Animate.stop(this._isDecelerating)
|
||||||
|
this._isDecelerating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct coordinates based on new zoom level
|
||||||
|
if (zoom != null && zoom !== this._zoomLevel) {
|
||||||
|
if (!this.options.zooming) {
|
||||||
|
warn('Zooming is not enabled!')
|
||||||
|
}
|
||||||
|
zoom = zoom ? zoom : 1
|
||||||
|
left *= zoom
|
||||||
|
top *= zoom
|
||||||
|
|
||||||
|
// // Recompute maximum values while temporary tweaking maximum scroll ranges
|
||||||
|
this._computeScrollMax(zoom)
|
||||||
|
} else {
|
||||||
|
// Keep zoom when not defined
|
||||||
|
zoom = this._zoomLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.scrollingX) {
|
||||||
|
left = this._scrollLeft
|
||||||
|
} else {
|
||||||
|
if (this.options.paging) {
|
||||||
|
left = Math.round(left / this._clientWidth) * this._clientWidth
|
||||||
|
} else if (this.options.snapping) {
|
||||||
|
left = Math.round(left / this._snapWidth) * this._snapWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.scrollingY) {
|
||||||
|
top = this._scrollTop
|
||||||
|
} else {
|
||||||
|
if (this.options.paging) {
|
||||||
|
top = Math.round(top / this._clientHeight) * this._clientHeight
|
||||||
|
} else if (this.options.snapping) {
|
||||||
|
top = Math.round(top / this._snapHeight) * this._snapHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit for allowed ranges
|
||||||
|
left = Math.max(Math.min(this._maxScrollLeft, left), 0)
|
||||||
|
top = Math.max(Math.min(this._maxScrollTop, top), 0)
|
||||||
|
|
||||||
|
// Don't animate when no change detected, still call publish to make sure
|
||||||
|
// that rendered position is really in-sync with internal data
|
||||||
|
if (left === this._scrollLeft && top === this._scrollTop) {
|
||||||
|
animate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish new values
|
||||||
|
if (!this._isTracking) {
|
||||||
|
this._publish(left, top, zoom, animate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zooms to the given level. Supports optional animation. Zooms
|
||||||
|
* the center when no coordinates are given.
|
||||||
|
*
|
||||||
|
* @param level {Number} Level to zoom to
|
||||||
|
* @param animate {Boolean ? false} Whether to use animation
|
||||||
|
* @param originLeft {Number ? null} Zoom in at given left coordinate
|
||||||
|
* @param originTop {Number ? null} Zoom in at given top coordinate
|
||||||
|
* @param callback {Function ? null} A callback that gets fired when the zoom is complete.
|
||||||
|
*/
|
||||||
|
zoomTo(level, animate, originLeft, originTop, callback) {
|
||||||
|
if (!this.options.zooming) {
|
||||||
|
warn('Zooming is not enabled!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add callback if exists
|
||||||
|
if (callback) {
|
||||||
|
this._zoomComplete = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop deceleration
|
||||||
|
if (this._isDecelerating) {
|
||||||
|
Animate.stop(this._isDecelerating)
|
||||||
|
this._isDecelerating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldLevel = this._zoomLevel
|
||||||
|
|
||||||
|
// Normalize input origin to center of viewport if not defined
|
||||||
|
if (originLeft == null) {
|
||||||
|
originLeft = this._clientWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originTop == null) {
|
||||||
|
originTop = this._clientHeight / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit level according to configuration
|
||||||
|
level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom)
|
||||||
|
|
||||||
|
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
||||||
|
this._computeScrollMax(level)
|
||||||
|
|
||||||
|
// Recompute left and top coordinates based on new zoom level
|
||||||
|
let left = (originLeft + this._scrollLeft) * level / oldLevel - originLeft
|
||||||
|
let top = (originTop + this._scrollTop) * level / oldLevel - originTop
|
||||||
|
|
||||||
|
// Limit x-axis
|
||||||
|
if (left > this._maxScrollLeft) {
|
||||||
|
left = this._maxScrollLeft
|
||||||
|
} else if (left < 0) {
|
||||||
|
left = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit y-axis
|
||||||
|
if (top > this._maxScrollTop) {
|
||||||
|
top = this._maxScrollTop
|
||||||
|
} else if (top < 0) {
|
||||||
|
top = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push values out
|
||||||
|
this._publish(left, top, level, animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
doTouchStart(touches, timeStamp) {
|
||||||
|
// Array-like check is enough here
|
||||||
|
if (touches.length == null) {
|
||||||
|
warn(`Invalid touch list: ${touches}`)
|
||||||
|
}
|
||||||
|
if (timeStamp instanceof Date) {
|
||||||
|
timeStamp = timeStamp.valueOf()
|
||||||
|
}
|
||||||
|
if (typeof timeStamp !== 'number') {
|
||||||
|
warn(`Invalid timestamp value: ${timeStamp}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset interruptedAnimation flag
|
||||||
|
this._interruptedAnimation = true
|
||||||
|
|
||||||
|
// Stop deceleration
|
||||||
|
if (this._isDecelerating) {
|
||||||
|
Animate.stop(this._isDecelerating)
|
||||||
|
this._isDecelerating = false
|
||||||
|
this._interruptedAnimation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop animation
|
||||||
|
if (this._isAnimating) {
|
||||||
|
Animate.stop(this._isAnimating)
|
||||||
|
this._isAnimating = false
|
||||||
|
this._interruptedAnimation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use center point when dealing with two fingers
|
||||||
|
const isSingleTouch = touches.length === 1
|
||||||
|
let currentTouchLeft, currentTouchTop
|
||||||
|
|
||||||
|
if (isSingleTouch) {
|
||||||
|
currentTouchLeft = touches[0].pageX
|
||||||
|
currentTouchTop = touches[0].pageY
|
||||||
|
} else {
|
||||||
|
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2
|
||||||
|
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store initial positions
|
||||||
|
this._initialTouchLeft = currentTouchLeft
|
||||||
|
this._initialTouchTop = currentTouchTop
|
||||||
|
|
||||||
|
// Store current zoom level
|
||||||
|
this._zoomLevelStart = this._zoomLevel
|
||||||
|
|
||||||
|
// Store initial touch positions
|
||||||
|
this._lastTouchLeft = currentTouchLeft
|
||||||
|
this._lastTouchTop = currentTouchTop
|
||||||
|
|
||||||
|
// Store initial move time stamp
|
||||||
|
this._lastTouchMove = timeStamp
|
||||||
|
|
||||||
|
// Reset initial scale
|
||||||
|
this._lastScale = 1
|
||||||
|
|
||||||
|
// Reset locking flags
|
||||||
|
this._enableScrollX = !isSingleTouch && this.options.scrollingX
|
||||||
|
this._enableScrollY = !isSingleTouch && this.options.scrollingY
|
||||||
|
|
||||||
|
// Reset tracking flag
|
||||||
|
this._isTracking = true
|
||||||
|
|
||||||
|
// Reset deceleration complete flag
|
||||||
|
this._didDecelerationComplete = false
|
||||||
|
|
||||||
|
// Dragging starts directly with two fingers, otherwise lazy with an offset
|
||||||
|
this._isDragging = !isSingleTouch
|
||||||
|
|
||||||
|
// Some features are disabled in multi touch scenarios
|
||||||
|
this._isSingleTouch = isSingleTouch
|
||||||
|
|
||||||
|
// Clearing data structure
|
||||||
|
this._positions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
doTouchMove(touches, timeStamp, scale) {
|
||||||
|
// Array-like check is enough here
|
||||||
|
if (touches.length == null) {
|
||||||
|
warn(`Invalid touch list: ${touches}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeStamp instanceof Date) {
|
||||||
|
timeStamp = timeStamp.valueOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof timeStamp !== 'number') {
|
||||||
|
warn(`Invalid timestamp value: ${timeStamp}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore event when tracking is not enabled (event might be outside of element)
|
||||||
|
if (!this._isTracking) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTouchLeft, currentTouchTop
|
||||||
|
|
||||||
|
// Compute move based around of center of fingers
|
||||||
|
if (touches.length === 2) {
|
||||||
|
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2
|
||||||
|
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2
|
||||||
|
} else {
|
||||||
|
currentTouchLeft = touches[0].pageX
|
||||||
|
currentTouchTop = touches[0].pageY
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = this._positions
|
||||||
|
|
||||||
|
// Are we already is dragging mode?
|
||||||
|
if (this._isDragging) {
|
||||||
|
// Compute move distance
|
||||||
|
const moveX = currentTouchLeft - this._lastTouchLeft
|
||||||
|
const moveY = currentTouchTop - this._lastTouchTop
|
||||||
|
|
||||||
|
// Read previous scroll position and zooming
|
||||||
|
let scrollLeft = this._scrollLeft
|
||||||
|
let scrollTop = this._scrollTop
|
||||||
|
let level = this._zoomLevel
|
||||||
|
|
||||||
|
// Work with scaling
|
||||||
|
if (scale != null && this.options.zooming) {
|
||||||
|
const oldLevel = level
|
||||||
|
|
||||||
|
// Recompute level based on previous scale and new scale
|
||||||
|
level = level / this.__lastScale * scale
|
||||||
|
|
||||||
|
// Limit level according to configuration
|
||||||
|
level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom)
|
||||||
|
|
||||||
|
// Only do further compution when change happened
|
||||||
|
if (oldLevel !== level) {
|
||||||
|
// Compute relative event position to container
|
||||||
|
var currentTouchLeftRel = currentTouchLeft - this._clientLeft
|
||||||
|
var currentTouchTopRel = currentTouchTop - this._clientTop
|
||||||
|
|
||||||
|
// Recompute left and top coordinates based on new zoom level
|
||||||
|
scrollLeft = (currentTouchLeftRel + scrollLeft) * level / oldLevel - currentTouchLeftRel
|
||||||
|
scrollTop = (currentTouchTopRel + scrollTop) * level / oldLevel - currentTouchTopRel
|
||||||
|
|
||||||
|
// Recompute max scroll values
|
||||||
|
this.__computeScrollMax(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._enableScrollX) {
|
||||||
|
scrollLeft -= moveX * this.options.speedMultiplier
|
||||||
|
const maxScrollLeft = this._maxScrollLeft
|
||||||
|
|
||||||
|
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
|
||||||
|
// Slow down on the edges
|
||||||
|
if (this.options.bouncing) {
|
||||||
|
scrollLeft += moveX / 2 * this.options.speedMultiplier
|
||||||
|
} else if (scrollLeft > maxScrollLeft) {
|
||||||
|
scrollLeft = maxScrollLeft
|
||||||
|
} else {
|
||||||
|
scrollLeft = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute new vertical scroll position
|
||||||
|
if (this._enableScrollY) {
|
||||||
|
scrollTop -= moveY * this.options.speedMultiplier
|
||||||
|
const maxScrollTop = this._maxScrollTop
|
||||||
|
if (scrollTop > maxScrollTop || scrollTop < 0) {
|
||||||
|
// Slow down on the edges
|
||||||
|
if (this.options.bouncing) {
|
||||||
|
scrollTop += moveY / 2 * this.options.speedMultiplier
|
||||||
|
} else if (scrollTop > maxScrollTop) {
|
||||||
|
scrollTop = maxScrollTop
|
||||||
|
} else {
|
||||||
|
scrollTop = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep list from growing infinitely (holding min 10, max 20 measure points)
|
||||||
|
if (positions.length > 60) {
|
||||||
|
positions.splice(0, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track scroll movement for decleration
|
||||||
|
positions.push(scrollLeft, scrollTop, timeStamp)
|
||||||
|
|
||||||
|
// Sync scroll position
|
||||||
|
this._publish(scrollLeft, scrollTop)
|
||||||
|
|
||||||
|
// Otherwise figure out whether we are switching into dragging mode now.
|
||||||
|
} else {
|
||||||
|
const minimumTrackingForScroll = this.options.locking ? 3 : 0
|
||||||
|
const minimumTrackingForDrag = 5
|
||||||
|
|
||||||
|
const distanceX = Math.abs(currentTouchLeft - this._initialTouchLeft)
|
||||||
|
const distanceY = Math.abs(currentTouchTop - this._initialTouchTop)
|
||||||
|
|
||||||
|
this._enableScrollX = this.options.scrollingX && distanceX >= minimumTrackingForScroll
|
||||||
|
this._enableScrollY = this.options.scrollingY && distanceY >= minimumTrackingForScroll
|
||||||
|
|
||||||
|
positions.push(this._scrollLeft, this._scrollTop, timeStamp)
|
||||||
|
|
||||||
|
this._isDragging =
|
||||||
|
(this._enableScrollX || this._enableScrollY) &&
|
||||||
|
(distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag)
|
||||||
|
if (this._isDragging) {
|
||||||
|
this._interruptedAnimation = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last touch positions and time stamp for next event
|
||||||
|
this._lastTouchLeft = currentTouchLeft
|
||||||
|
this._lastTouchTop = currentTouchTop
|
||||||
|
this._lastTouchMove = timeStamp
|
||||||
|
}
|
||||||
|
|
||||||
|
doTouchEnd(timeStamp) {
|
||||||
|
if (timeStamp instanceof Date) {
|
||||||
|
timeStamp = timeStamp.valueOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof timeStamp !== 'number') {
|
||||||
|
warn(`Invalid timestamp value: ${timeStamp}`)
|
||||||
|
}
|
||||||
|
// Ignore event when tracking is not enabled (no touchstart event on element)
|
||||||
|
// This is required as this listener ('touchmove') sits on the document and not on the element itthis.
|
||||||
|
if (!this._isTracking) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not touching anymore (when two finger hit the screen there are two touch end events)
|
||||||
|
this._isTracking = false
|
||||||
|
|
||||||
|
// Be sure to reset the dragging flag now. Here we also detect whether
|
||||||
|
// the finger has moved fast enough to switch into a deceleration animation.
|
||||||
|
if (this._isDragging) {
|
||||||
|
// Reset dragging flag
|
||||||
|
this._isDragging = false
|
||||||
|
|
||||||
|
// Start deceleration
|
||||||
|
// Verify that the last move detected was in some relevant time frame
|
||||||
|
if (this._isSingleTouch && this.options.animating && timeStamp - this._lastTouchMove <= 100) {
|
||||||
|
// Then figure out what the scroll position was about 100ms ago
|
||||||
|
const positions = this._positions
|
||||||
|
const endPos = positions.length - 1
|
||||||
|
let startPos = endPos
|
||||||
|
|
||||||
|
// Move pointer to position measured 100ms ago
|
||||||
|
for (let i = endPos; i > 0 && positions[i] > this._lastTouchMove - 100; i -= 3) {
|
||||||
|
startPos = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// If start and stop position is identical in a 100ms timeframe,
|
||||||
|
// we cannot compute any useful deceleration.
|
||||||
|
if (startPos !== endPos) {
|
||||||
|
// Compute relative movement between these two points
|
||||||
|
const timeOffset = positions[endPos] - positions[startPos]
|
||||||
|
const movedLeft = this._scrollLeft - positions[startPos - 2]
|
||||||
|
const movedTop = this._scrollTop - positions[startPos - 1]
|
||||||
|
|
||||||
|
// Based on 50ms compute the movement to apply for each render step
|
||||||
|
this._decelerationVelocityX = movedLeft / timeOffset * (1000 / 60)
|
||||||
|
this._decelerationVelocityY = movedTop / timeOffset * (1000 / 60)
|
||||||
|
|
||||||
|
// How much velocity is required to start the deceleration
|
||||||
|
const minVelocityToStartDeceleration = this.options.paging || this.options.snapping ? 4 : 1
|
||||||
|
|
||||||
|
// Verify that we have enough velocity to start deceleration
|
||||||
|
if (
|
||||||
|
Math.abs(this._decelerationVelocityX) > minVelocityToStartDeceleration ||
|
||||||
|
Math.abs(this._decelerationVelocityY) > minVelocityToStartDeceleration
|
||||||
|
) {
|
||||||
|
// Deactivate pull-to-refresh when decelerating
|
||||||
|
if (!this._refreshActive) {
|
||||||
|
this._startDeceleration(timeStamp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.options.scrollingComplete()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.options.scrollingComplete()
|
||||||
|
}
|
||||||
|
} else if (timeStamp - this._lastTouchMove > 100) {
|
||||||
|
!this.options.snapping && this.options.scrollingComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this was a slower move it is per default non decelerated, but this
|
||||||
|
// still means that we want snap back to the bounds which is done here.
|
||||||
|
// This is placed outside the condition above to improve edge case stability
|
||||||
|
// e.g. touchend fired without enabled dragging. This should normally do not
|
||||||
|
// have modified the scroll positions or even showed the scrollbars though.
|
||||||
|
if (!this._isDecelerating) {
|
||||||
|
if (this._refreshActive && this._refreshStart) {
|
||||||
|
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
||||||
|
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
||||||
|
this._publish(this._scrollLeft, -this._refreshHeight, this._zoomLevel, true)
|
||||||
|
|
||||||
|
if (this._refreshStart) {
|
||||||
|
this._refreshStart()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this._interruptedAnimation || this._isDragging) {
|
||||||
|
this.options.scrollingComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollTo(this._scrollLeft, this._scrollTop, true, this._zoomLevel)
|
||||||
|
// Directly signalize deactivation (nothing todo on refresh?)
|
||||||
|
if (this._refreshActive) {
|
||||||
|
this._refreshActive = false
|
||||||
|
if (this._refreshDeactivate) {
|
||||||
|
this._refreshDeactivate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fully cleanup list
|
||||||
|
this._positions.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_publish(left, top, zoom = 1, animate = false) {
|
||||||
|
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
|
||||||
|
const wasAnimating = this._isAnimating
|
||||||
|
|
||||||
|
if (wasAnimating) {
|
||||||
|
Animate.stop(wasAnimating)
|
||||||
|
this._isAnimating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animate && this.options.animating) {
|
||||||
|
// Keep scheduled positions for scrollBy/zoomBy functionality
|
||||||
|
this._scheduledLeft = left
|
||||||
|
this._scheduledTop = top
|
||||||
|
this._scheduledZoom = zoom
|
||||||
|
|
||||||
|
const oldLeft = this._scrollLeft
|
||||||
|
const oldTop = this._scrollTop
|
||||||
|
const oldZoom = this._zoomLevel
|
||||||
|
|
||||||
|
const diffLeft = left - oldLeft
|
||||||
|
const diffTop = top - oldTop
|
||||||
|
const diffZoom = zoom - oldZoom
|
||||||
|
|
||||||
|
const step = (percent, now, render) => {
|
||||||
|
if (render) {
|
||||||
|
this._scrollLeft = oldLeft + diffLeft * percent
|
||||||
|
this._scrollTop = oldTop + diffTop * percent
|
||||||
|
this._zoomLevel = oldZoom + diffZoom * percent
|
||||||
|
// Push values out
|
||||||
|
if (this._callback) {
|
||||||
|
this._callback(this._scrollLeft, this._scrollTop, this._zoomLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verify = id => {
|
||||||
|
return this._isAnimating === id
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = (renderedFramesPerSecond, animationId, wasFinished) => {
|
||||||
|
if (animationId === this._isAnimating) {
|
||||||
|
this._isAnimating = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._didDecelerationComplete || wasFinished) {
|
||||||
|
this.options.scrollingComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.zooming) {
|
||||||
|
this._computeScrollMax()
|
||||||
|
if (this._zoomComplete) {
|
||||||
|
this._zoomComplete()
|
||||||
|
this._zoomComplete = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
|
||||||
|
this._isAnimating = Animate.start(
|
||||||
|
step,
|
||||||
|
verify,
|
||||||
|
completed,
|
||||||
|
this.options.animationDuration,
|
||||||
|
wasAnimating ? easeOutCubic : easeInOutCubic,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this._scheduledLeft = this._scrollLeft = left
|
||||||
|
this._scheduledTop = this._scrollTop = top
|
||||||
|
this._scheduledZoom = this._zoomLevel = zoom
|
||||||
|
|
||||||
|
// Push values out
|
||||||
|
if (this._callback) {
|
||||||
|
this._callback(left, top)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix max scroll ranges
|
||||||
|
if (this.options.zooming) {
|
||||||
|
this._computeScrollMax()
|
||||||
|
if (this._zoomComplete) {
|
||||||
|
this._zoomComplete()
|
||||||
|
this._zoomComplete = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeScrollMax(zoomLevel) {
|
||||||
|
if (zoomLevel == null) {
|
||||||
|
zoomLevel = this._zoomLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
this._maxScrollLeft = Math.max(this._contentWidth * zoomLevel - this._clientWidth, 0)
|
||||||
|
this._maxScrollTop = Math.max(this._contentHeight * zoomLevel - this._clientHeight, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
_startDeceleration(timeStamp) {
|
||||||
|
if (this.options.paging) {
|
||||||
|
const scrollLeft = Math.max(Math.min(this._scrollLeft, this._maxScrollLeft), 0)
|
||||||
|
const scrollTop = Math.max(Math.min(this._scrollTop, this._maxScrollTop), 0)
|
||||||
|
const clientWidth = this._clientWidth
|
||||||
|
const clientHeight = this._clientHeight
|
||||||
|
|
||||||
|
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
|
||||||
|
// Each page should have exactly the size of the client area.
|
||||||
|
this._minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth
|
||||||
|
this._minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight
|
||||||
|
this._maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth
|
||||||
|
this._maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight
|
||||||
|
} else {
|
||||||
|
this._minDecelerationScrollLeft = 0
|
||||||
|
this._minDecelerationScrollTop = 0
|
||||||
|
this._maxDecelerationScrollLeft = this._maxScrollLeft
|
||||||
|
this._maxDecelerationScrollTop = this._maxScrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap class method
|
||||||
|
const step = (percent, now, render) => {
|
||||||
|
this._stepThroughDeceleration(render)
|
||||||
|
}
|
||||||
|
|
||||||
|
// How much velocity is required to keep the deceleration running
|
||||||
|
const minVelocityToKeepDecelerating = this.options.snapping ? 4 : 0.001
|
||||||
|
|
||||||
|
// Detect whether it's still worth to continue animating steps
|
||||||
|
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
|
||||||
|
const verify = () => {
|
||||||
|
const shouldContinue =
|
||||||
|
Math.abs(this._decelerationVelocityX) >= minVelocityToKeepDecelerating ||
|
||||||
|
Math.abs(this._decelerationVelocityY) >= minVelocityToKeepDecelerating
|
||||||
|
if (!shouldContinue) {
|
||||||
|
this._didDecelerationComplete = true
|
||||||
|
}
|
||||||
|
return shouldContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = (renderedFramesPerSecond, animationId, wasFinished) => {
|
||||||
|
this._isDecelerating = false
|
||||||
|
// if (this._didDecelerationComplete) {
|
||||||
|
// this.options.scrollingComplete()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
|
||||||
|
this.scrollTo(this._scrollLeft, this._scrollTop, this.options.snapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animation and switch on flag
|
||||||
|
this._isDecelerating = Animate.start(step, verify, completed)
|
||||||
|
}
|
||||||
|
|
||||||
|
_stepThroughDeceleration(render) {
|
||||||
|
//
|
||||||
|
// COMPUTE NEXT SCROLL POSITION
|
||||||
|
//
|
||||||
|
|
||||||
|
// Add deceleration to scroll position
|
||||||
|
let scrollLeft = this._scrollLeft + this._decelerationVelocityX
|
||||||
|
let scrollTop = this._scrollTop + this._decelerationVelocityY
|
||||||
|
|
||||||
|
//
|
||||||
|
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
|
||||||
|
//
|
||||||
|
|
||||||
|
if (!this.options.bouncing) {
|
||||||
|
var scrollLeftFixed = Math.max(
|
||||||
|
Math.min(this._maxDecelerationScrollLeft, scrollLeft),
|
||||||
|
this._minDecelerationScrollLeft,
|
||||||
|
)
|
||||||
|
if (scrollLeftFixed !== scrollLeft) {
|
||||||
|
scrollLeft = scrollLeftFixed
|
||||||
|
this._decelerationVelocityX = 0
|
||||||
|
}
|
||||||
|
var scrollTopFixed = Math.max(Math.min(this._maxDecelerationScrollTop, scrollTop), this._minDecelerationScrollTop)
|
||||||
|
if (scrollTopFixed !== scrollTop) {
|
||||||
|
scrollTop = scrollTopFixed
|
||||||
|
this._decelerationVelocityY = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// UPDATE SCROLL POSITION
|
||||||
|
//
|
||||||
|
|
||||||
|
if (render) {
|
||||||
|
this._publish(scrollLeft, scrollTop, this._zoomLevel)
|
||||||
|
} else {
|
||||||
|
this._scrollLeft = scrollLeft
|
||||||
|
this._scrollTop = scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// SLOW DOWN
|
||||||
|
//
|
||||||
|
|
||||||
|
// Slow down velocity on every iteration
|
||||||
|
if (!this.options.paging) {
|
||||||
|
// This is the factor applied to every iteration of the animation
|
||||||
|
// to slow down the process. This should emulate natural behavior where
|
||||||
|
// objects slow down when the initiator of the movement is removed
|
||||||
|
var frictionFactor = 0.95
|
||||||
|
this._decelerationVelocityX *= frictionFactor
|
||||||
|
this._decelerationVelocityY *= frictionFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// BOUNCING SUPPORT
|
||||||
|
//
|
||||||
|
|
||||||
|
if (this.options.bouncing) {
|
||||||
|
var scrollOutsideX = 0
|
||||||
|
var scrollOutsideY = 0
|
||||||
|
|
||||||
|
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
|
||||||
|
var penetrationDeceleration = this.options.penetrationDeceleration
|
||||||
|
var penetrationAcceleration = this.options.penetrationAcceleration
|
||||||
|
|
||||||
|
// Check limits
|
||||||
|
if (scrollLeft < this._minDecelerationScrollLeft) {
|
||||||
|
scrollOutsideX = this._minDecelerationScrollLeft - scrollLeft
|
||||||
|
} else if (scrollLeft > this._maxDecelerationScrollLeft) {
|
||||||
|
scrollOutsideX = this._maxDecelerationScrollLeft - scrollLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollTop < this._minDecelerationScrollTop) {
|
||||||
|
scrollOutsideY = this._minDecelerationScrollTop - scrollTop
|
||||||
|
} else if (scrollTop > this._maxDecelerationScrollTop) {
|
||||||
|
scrollOutsideY = this._maxDecelerationScrollTop - scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow down until slow enough, then flip back to snap position
|
||||||
|
if (scrollOutsideX !== 0) {
|
||||||
|
if (scrollOutsideX * this._decelerationVelocityX <= 0) {
|
||||||
|
this._decelerationVelocityX += scrollOutsideX * penetrationDeceleration
|
||||||
|
} else {
|
||||||
|
this._decelerationVelocityX = scrollOutsideX * penetrationAcceleration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollOutsideY !== 0) {
|
||||||
|
if (scrollOutsideY * this._decelerationVelocityY <= 0) {
|
||||||
|
this._decelerationVelocityY += scrollOutsideY * penetrationDeceleration
|
||||||
|
} else {
|
||||||
|
this._decelerationVelocityY = scrollOutsideY * penetrationAcceleration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(Scroller.prototype, members)
|
|
@ -0,0 +1,128 @@
|
||||||
|
import {noop} from './lang'
|
||||||
|
/**
|
||||||
|
* Mix properties into target object.
|
||||||
|
*/
|
||||||
|
export function extend(to, _from) {
|
||||||
|
for (const key in _from) {
|
||||||
|
to[key] = _from[key]
|
||||||
|
}
|
||||||
|
return to
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiple Array traversal
|
||||||
|
* @return 1 continue
|
||||||
|
* @return 2 break
|
||||||
|
*/
|
||||||
|
export function traverse(data, childrenKeys = [], fn = noop) {
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof childrenKeys === 'function') {
|
||||||
|
fn = childrenKeys
|
||||||
|
childrenKeys = []
|
||||||
|
}
|
||||||
|
let level = 0 // current level
|
||||||
|
let indexs = [] // index set of all levels
|
||||||
|
const walk = curData => {
|
||||||
|
for (let i = 0, len = curData.length; i < len; i++) {
|
||||||
|
const isArray = Array.isArray(curData[i])
|
||||||
|
const key = Array.isArray(childrenKeys) ? childrenKeys[level] : childrenKeys
|
||||||
|
if (isArray || (curData[i] && curData[i][key])) {
|
||||||
|
level++
|
||||||
|
indexs.push(i)
|
||||||
|
walk(isArray ? curData[i] : curData[i][key])
|
||||||
|
} else if (level >= childrenKeys.length) {
|
||||||
|
const res = fn(curData[i], level, [...indexs, i])
|
||||||
|
if (res === 1) {
|
||||||
|
continue
|
||||||
|
} else if (res === 2) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
level = 0
|
||||||
|
indexs = []
|
||||||
|
}
|
||||||
|
walk(data)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Merge an Array of Objects into a single Object.
|
||||||
|
*/
|
||||||
|
export function toObject(arr) {
|
||||||
|
const res = {}
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i]) {
|
||||||
|
extend(res, arr[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an Array-like object to a real Array.
|
||||||
|
*/
|
||||||
|
export function toArray(list, start) {
|
||||||
|
start = start || 0
|
||||||
|
let i = list.length - start
|
||||||
|
const ret = []
|
||||||
|
while (i--) {
|
||||||
|
ret.unshift(list[i + start])
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether item is in list or list equal item
|
||||||
|
*/
|
||||||
|
export function inArray(list, item) {
|
||||||
|
return Array.isArray(list) ? !!~list.indexOf(item) : item === list
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a input value to a number for persistence.
|
||||||
|
* If the conversion fails, return original string.
|
||||||
|
*/
|
||||||
|
export function toNumber(val) {
|
||||||
|
const n = parseFloat(val)
|
||||||
|
return isNaN(n) ? val : n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value to a string
|
||||||
|
*/
|
||||||
|
export function toString(val) {
|
||||||
|
return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the two objects are equal or not shallowly
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function compareObjects(object0, object1) {
|
||||||
|
let ret = true
|
||||||
|
|
||||||
|
if (!object0 || !object1) {
|
||||||
|
ret = false
|
||||||
|
} else if (typeof object0 !== 'object' || typeof object1 !== 'object') {
|
||||||
|
ret = false
|
||||||
|
} else if (JSON.stringify(object0) !== JSON.stringify(object1)) {
|
||||||
|
ret = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check object is empty
|
||||||
|
*/
|
||||||
|
export function isEmptyObject(obj) {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
title: ActionBar 操作栏
|
||||||
|
preview: https://didi.github.io/mand-mobile/examples/action-bar
|
||||||
|
---
|
||||||
|
|
||||||
|
汇集若干文案或操作按钮的吸底边栏,可用于展示表单信息与提交按钮
|
||||||
|
|
||||||
|
### 引入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ActionBar } from 'mand-mobile'
|
||||||
|
|
||||||
|
Vue.component(ActionBar.name, ActionBar)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
<!-- DEMO -->
|
||||||
|
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### ActionBar Props
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||||
|
|----|-----|------|------|------|
|
||||||
|
|actions|按钮组|Array<{text, disabled, onClick}>|-|`text`为按钮文案,<br/>`disabled`为是否禁用改按钮,<br/>`onClick`为点击事件响应函数,传参数与`click`事件相同|
|
||||||
|
|has-text|是否显示文案|Boolean|是否含有`slot`|文案可通过`slot`传入|
|
||||||
|
|
||||||
|
|
||||||
|
#### ActionBar Events
|
||||||
|
|
||||||
|
##### @click(event, action)
|
||||||
|
按钮点击事件
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 |
|
||||||
|
|----|-----|------|
|
||||||
|
|action|actions列表中与被点击按钮对应的对象|Object: {text, disabled, ...}|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
'name': 'action-bar',
|
||||||
|
'text': '操作栏',
|
||||||
|
'category': 'basic',
|
||||||
|
'description': '',
|
||||||
|
'author': 'xuxiaoyan'
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-action-bar md-example-child-0">
|
||||||
|
<md-action-bar :actions="data"></md-action-bar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {ActionBar, Toast} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: '通栏多按钮',
|
||||||
|
name: 'action-bar-demo',
|
||||||
|
height: 150,
|
||||||
|
components: {
|
||||||
|
[ActionBar.name]: ActionBar,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
text: '操作一',
|
||||||
|
onClick: this.handleClick,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '操作二',
|
||||||
|
onClick: this.handleClick,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
Toast.succeed('Click')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-action-bar md-example-child-1">
|
||||||
|
<md-action-bar :actions="data"></md-action-bar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {ActionBar, Toast} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: '通栏多按钮禁用',
|
||||||
|
name: 'action-bar-demo',
|
||||||
|
height: 150,
|
||||||
|
components: {
|
||||||
|
[ActionBar.name]: ActionBar,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
text: '操作一',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '操作二',
|
||||||
|
onClick: this.handleClick,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '操作三',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
Toast.succeed('Click')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-action-bar md-example-child-2">
|
||||||
|
<md-action-bar :actions="data" @click="onBtnClick">
|
||||||
|
¥128.00
|
||||||
|
</md-action-bar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {ActionBar, Dialog} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: '通栏带文案',
|
||||||
|
name: 'action-bar-demo',
|
||||||
|
height: 150,
|
||||||
|
components: {
|
||||||
|
[ActionBar.name]: ActionBar,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
text: '操作',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onBtnClick(event, action) {
|
||||||
|
Dialog.alert({
|
||||||
|
content: `${action.text}完成`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example action-bar">
|
||||||
|
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||||
|
<div class="md-example-title" v-html="demo.title"></div>
|
||||||
|
<div class="md-example-content">
|
||||||
|
<component :is="demo"></component>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||||
|
import Demo0 from './cases/demo0'
|
||||||
|
import Demo1 from './cases/demo1'
|
||||||
|
import Demo2 from './cases/demo2'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...createDemoModule('action-bar', [Demo0, Demo1, Demo2]),
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-example.action-bar
|
||||||
|
.md-example-section
|
||||||
|
.md-action-bar
|
||||||
|
position relative !important
|
||||||
|
</style>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="md-action-bar"
|
||||||
|
:class="{'with-text': hasText, 'multi-action': !!this.actions.length }">
|
||||||
|
<div class="md-action-bar-text" v-if="hasText">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="md-action-bar-button">
|
||||||
|
<template v-for="(item, index) in actions">
|
||||||
|
<div
|
||||||
|
class="button-item"
|
||||||
|
:key="index"
|
||||||
|
:class="{disabled: !!item.disabled}"
|
||||||
|
@click="$_onBtnClick($event, item)"
|
||||||
|
v-html="item.text"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {isEmptyObject} from '../_util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'md-action-bar',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
hasText: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return !isEmptyObject(this.$slots)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// MARK: events handler
|
||||||
|
$_onBtnClick(event, action) {
|
||||||
|
if (action.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action.onClick && action.onClick(event, action)
|
||||||
|
this.$emit('click', event, action)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-action-bar
|
||||||
|
position fixed
|
||||||
|
z-index action-bar-zindex
|
||||||
|
left 0
|
||||||
|
bottom 0
|
||||||
|
width action-bar-width
|
||||||
|
height action-bar-height
|
||||||
|
padding-bottom constant(safe-area-inset-bottom)
|
||||||
|
background color-bg-base
|
||||||
|
clearfix()
|
||||||
|
.md-action-bar-text
|
||||||
|
float left
|
||||||
|
display flex
|
||||||
|
height 100%
|
||||||
|
width 65%
|
||||||
|
padding-left h-gap-lg
|
||||||
|
align-items center
|
||||||
|
overflow hidden
|
||||||
|
color action-bar-text-color
|
||||||
|
font-size action-bar-text-font-size
|
||||||
|
box-sizing border-box
|
||||||
|
.md-action-bar-button
|
||||||
|
display flex
|
||||||
|
height 100%
|
||||||
|
.button-item
|
||||||
|
display flex
|
||||||
|
float right
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
flex 1
|
||||||
|
color action-bar-button-color
|
||||||
|
font-size action-bar-button-font-size
|
||||||
|
font-weight font-weight-medium
|
||||||
|
background action-bar-button-fill
|
||||||
|
hairline(right, color-border-base)
|
||||||
|
-webkit-user-select none
|
||||||
|
-webkit-tap-highlight-color transparent
|
||||||
|
&.disabled
|
||||||
|
opacity opacity-disabled
|
||||||
|
&:nth-last-of-type(2)
|
||||||
|
&::before
|
||||||
|
display none
|
||||||
|
&:last-of-type
|
||||||
|
background action-bar-button-fill-hightlight
|
||||||
|
color action-bar-button-color-hightlight
|
||||||
|
&.disabled
|
||||||
|
background color-bg-disabled
|
||||||
|
color color-text-base-inverse
|
||||||
|
&::before
|
||||||
|
display none
|
||||||
|
&::after
|
||||||
|
absolute-pos()
|
||||||
|
display none
|
||||||
|
content ''
|
||||||
|
position absolute
|
||||||
|
box-sizing border-box
|
||||||
|
background-color color-bg-tap
|
||||||
|
&:active::after
|
||||||
|
display block
|
||||||
|
|
||||||
|
&.with-text.multi-action
|
||||||
|
.md-action-bar-text
|
||||||
|
width 40%
|
||||||
|
</style>
|
|
@ -0,0 +1,65 @@
|
||||||
|
import ActionBar from '../index'
|
||||||
|
import {mount} from 'avoriaz'
|
||||||
|
|
||||||
|
describe('ActionBar', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a action-bar', () => {
|
||||||
|
wrapper = mount(ActionBar, {
|
||||||
|
propsData: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const buttons = wrapper.find('.button-item')
|
||||||
|
const button = wrapper.find('.button-item')[0]
|
||||||
|
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||||
|
button.trigger('click')
|
||||||
|
expect(buttons.length).to.equal(2)
|
||||||
|
expect(eventStub.calledOnce).to.be.true
|
||||||
|
expect(eventStub.calledWith('click')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a action-bar with disabled button', () => {
|
||||||
|
wrapper = mount(ActionBar, {
|
||||||
|
propsData: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: '1',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const button0 = wrapper.find('.button-item')[0]
|
||||||
|
expect(button0.hasClass('disabled')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a action-bar with text', () => {
|
||||||
|
wrapper = mount(ActionBar, {
|
||||||
|
propsData: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hasText: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const text = wrapper.find('.md-action-bar-text')
|
||||||
|
expect(text.length > 0).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
title: ActionSheet 动作面板
|
||||||
|
preview: https://didi.github.io/mand-mobile/examples/action-sheet
|
||||||
|
---
|
||||||
|
|
||||||
|
用于提供场景相关的多个操作动作
|
||||||
|
|
||||||
|
### 引入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ActionSheet } from 'mand-mobile'
|
||||||
|
|
||||||
|
Vue.component(ActionSheet.name, ActionSheet)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
<!-- DEMO -->
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### ActionSheet Props
|
||||||
|
|属性 | 说明 | 类型 | 默认值 |
|
||||||
|
|----|-----|------|------|
|
||||||
|
|v-model|面板是否可见|Boolean| `false`|
|
||||||
|
|title|面板标题|String|- |
|
||||||
|
|options|面板选项| Array<{text, value}>| `[]`|
|
||||||
|
|default-index|默认选中项| Boolean| `0`|
|
||||||
|
|invalid-index|禁用选择项索引 |Number|`-1`|
|
||||||
|
|cancel-text|取消按钮文案 |String |-|
|
||||||
|
|
||||||
|
#### ActionSheet Events
|
||||||
|
|
||||||
|
##### @selected(item)
|
||||||
|
选择事件
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 |
|
||||||
|
|----|-----|------|
|
||||||
|
|item| 选中项的值 | Object: {text, value} |
|
||||||
|
|
||||||
|
##### @cancel()
|
||||||
|
取消选择事件
|
||||||
|
|
||||||
|
##### @show()
|
||||||
|
面板展示事件
|
||||||
|
|
||||||
|
##### @hide()
|
||||||
|
面板隐藏事件
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
'name': 'action-sheet',
|
||||||
|
'text': '底部弹窗',
|
||||||
|
'category': 'feedback',
|
||||||
|
'description': '',
|
||||||
|
'author': 'qiman'
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-action-sheet">
|
||||||
|
<md-button @click.native="$_showActionSheet">唤起动作面板</md-button>
|
||||||
|
<md-action-sheet
|
||||||
|
v-model="value"
|
||||||
|
:title="title"
|
||||||
|
:default-index="defaultIndex"
|
||||||
|
:invalid-index="invalidIndex"
|
||||||
|
:cancel-text="cancelText"
|
||||||
|
:options="options"
|
||||||
|
@selected="$_selected"
|
||||||
|
@cancel="$_cancel"
|
||||||
|
></md-action-sheet>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {ActionSheet, Button, Dialog} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'action-sheet-demo',
|
||||||
|
height: 500,
|
||||||
|
components: {
|
||||||
|
[ActionSheet.name]: ActionSheet,
|
||||||
|
[Button.name]: Button,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: false,
|
||||||
|
title: '操作说明的title',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '选项1',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项2',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项3',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultIndex: 1,
|
||||||
|
invalidIndex: 2,
|
||||||
|
cancelText: '取消',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
$_showActionSheet() {
|
||||||
|
this.value = true
|
||||||
|
},
|
||||||
|
$_selected(item) {
|
||||||
|
Dialog.alert({
|
||||||
|
content: `selected: ${JSON.stringify(item)}`,
|
||||||
|
})
|
||||||
|
console.log('action-sheet selected:', JSON.stringify(item))
|
||||||
|
},
|
||||||
|
$_cancel() {
|
||||||
|
Dialog.alert({
|
||||||
|
content: 'cancel',
|
||||||
|
})
|
||||||
|
console.log('action-sheet cancel')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example action-sheet">
|
||||||
|
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||||
|
<div class="md-example-title" v-html="demo.title"></div>
|
||||||
|
<div class="md-example-content">
|
||||||
|
<component :is="demo"></component>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||||
|
import Demo0 from './cases/demo0'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...createDemoModule('action-sheet', [Demo0]),
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,146 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-action-sheet">
|
||||||
|
<md-popup
|
||||||
|
v-model="isActionSheetShow"
|
||||||
|
position="bottom"
|
||||||
|
prevent-scroll
|
||||||
|
@show="$_onShow"
|
||||||
|
@hide="$_onHide"
|
||||||
|
>
|
||||||
|
<div class="md-action-sheet-content">
|
||||||
|
<header v-if="title">{{ title }}</header>
|
||||||
|
<ul>
|
||||||
|
<template v-for="(item, index) in options">
|
||||||
|
<li
|
||||||
|
:key="index"
|
||||||
|
:class="{
|
||||||
|
'active': index === clickIndex,
|
||||||
|
'disabled': index=== invalidIndex,
|
||||||
|
'md-action-sheet-item': true
|
||||||
|
}"
|
||||||
|
@click="$_onSelect(item, index)"
|
||||||
|
v-html="item.text || item.label"
|
||||||
|
></li>
|
||||||
|
</template>
|
||||||
|
<li class="cancel-btn" @click="$_onCancel">{{ cancelText }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</md-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import Popup from '../popup'
|
||||||
|
import {inArray} from '../_util'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'md-action-sheet',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
[Popup.name]: Popup,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
invalidIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
cancelText: {
|
||||||
|
type: String,
|
||||||
|
default: '取消',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isActionSheetShow: this.value,
|
||||||
|
clickIndex: -1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.isActionSheetShow = newVal
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.clickIndex = this.defaultIndex
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// MARK: events handler, 如 $_onButtonClick
|
||||||
|
$_onShow() {
|
||||||
|
this.$emit('show')
|
||||||
|
},
|
||||||
|
$_onHide() {
|
||||||
|
this.$emit('hide')
|
||||||
|
this.$emit('input', false)
|
||||||
|
},
|
||||||
|
$_onSelect(item, index) {
|
||||||
|
if (index === this.invalidIndex || inArray(this.invalidIndex, index)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.clickIndex = index
|
||||||
|
this.$emit('selected', item)
|
||||||
|
this.$emit('input', false)
|
||||||
|
},
|
||||||
|
$_onCancel() {
|
||||||
|
this.$emit('cancel')
|
||||||
|
this.$emit('input', false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.md-action-sheet
|
||||||
|
color color-text-base
|
||||||
|
-webkit-font-smoothing antialiased
|
||||||
|
.md-action-sheet-content
|
||||||
|
overflow hidden
|
||||||
|
position relative
|
||||||
|
width 100%
|
||||||
|
font-size action-sheet-font-size
|
||||||
|
background color-bg-base
|
||||||
|
text-align center
|
||||||
|
header
|
||||||
|
vertical-height(action-sheet-height)
|
||||||
|
padding 0 30px
|
||||||
|
word-ellipsis()
|
||||||
|
>ul
|
||||||
|
li
|
||||||
|
vertical-height(action-sheet-height)
|
||||||
|
hairline(top, color-border-base)
|
||||||
|
box-sizing border-box
|
||||||
|
font-size font-body-normal
|
||||||
|
&.active
|
||||||
|
color button-primary-fill
|
||||||
|
&.disabled
|
||||||
|
opacity field-item-color-disabled
|
||||||
|
&.cancel-btn
|
||||||
|
height 132px
|
||||||
|
line-height 120px
|
||||||
|
&::before
|
||||||
|
display block
|
||||||
|
content ''
|
||||||
|
height 12px
|
||||||
|
background color-primary-background
|
||||||
|
</style>
|
|
@ -0,0 +1,128 @@
|
||||||
|
import ActionSheet from '../index'
|
||||||
|
import {mount} from 'avoriaz'
|
||||||
|
|
||||||
|
describe('ActionSheet', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a action-sheet', done => {
|
||||||
|
wrapper = mount(ActionSheet, {
|
||||||
|
propsData: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '选项1',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项2',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项3',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('md-action-sheet')).to.be.true
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(wrapper.find('.md-action-sheet-item').length).to.equal(3)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a action-sheet with defaultIndex', done => {
|
||||||
|
wrapper = mount(ActionSheet, {
|
||||||
|
propsData: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '选项1',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项2',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项3',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
value: true,
|
||||||
|
defaultIndex: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
expect(wrapper.find('.md-action-sheet-item')[1].hasClass('active')).to.equal(true)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('action-sheet events selected', done => {
|
||||||
|
wrapper = mount(ActionSheet, {
|
||||||
|
propsData: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '选项1',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项2',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项3',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
value: true,
|
||||||
|
activeIndex: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
wrapper.find('.md-action-sheet-item')[0].trigger('click')
|
||||||
|
expect(wrapper.vm.clickIndex).equal(0)
|
||||||
|
expect(eventStub.calledWith('selected')).to.be.true
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selector events cancel', done => {
|
||||||
|
wrapper = mount(ActionSheet, {
|
||||||
|
propsData: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '选项1',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项2',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '选项3',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
value: true,
|
||||||
|
activeIndex: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||||
|
|
||||||
|
wrapper.vm.$nextTick(() => {
|
||||||
|
const cancelBtn = wrapper.find('.cancel-btn')[0]
|
||||||
|
cancelBtn.trigger('click')
|
||||||
|
expect(eventStub.calledWith('cancel')).to.be.true
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
title: Agree 勾选按钮
|
||||||
|
preview: https://didi.github.io/mand-mobile/examples/agree
|
||||||
|
---
|
||||||
|
|
||||||
|
用于标记切换某种状态,如协议勾选
|
||||||
|
|
||||||
|
### 引入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Agree } from 'mand-mobile'
|
||||||
|
|
||||||
|
Vue.component(Agree.name, Agree)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
<!-- DEMO -->
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### Agree Props
|
||||||
|
|属性 | 说明 | 类型 | 默认值 |
|
||||||
|
|----|-----|------|------|
|
||||||
|
|v-model|是否选中|Boolean|`false`|
|
||||||
|
|disabled|是否禁用|Boolean|`false`|
|
||||||
|
|size|按钮大小,可选值同icon|String|`md`|
|
||||||
|
|
||||||
|
#### Agree Events
|
||||||
|
|
||||||
|
##### @change(name, checked)
|
||||||
|
勾选状态发生变化事件
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 |
|
||||||
|
|----|-----|------|
|
||||||
|
|name|单选按钮名称,唯一标识|Number/String|
|
||||||
|
|checked|是否选中|Boolean|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
'name': 'agree',
|
||||||
|
'text': '单选框',
|
||||||
|
'category': 'form',
|
||||||
|
'description': '',
|
||||||
|
'author': 'chengyanjing'
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-agree md-example-child-0">
|
||||||
|
<md-agree
|
||||||
|
v-model="agreeConf.checked"
|
||||||
|
:disabled="agreeConf.disabled"
|
||||||
|
:size="agreeConf.size"
|
||||||
|
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||||
|
>
|
||||||
|
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||||
|
</md-agree>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Agree} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'agree-demo',
|
||||||
|
title: '选中状态',
|
||||||
|
height: 120,
|
||||||
|
components: {
|
||||||
|
[Agree.name]: Agree,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
agreeConf: {
|
||||||
|
checked: true,
|
||||||
|
name: 'agree0',
|
||||||
|
size: 'lg',
|
||||||
|
disabled: false,
|
||||||
|
introduction: '选中状态',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(name, checked) {
|
||||||
|
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-agree md-example-child-1">
|
||||||
|
<md-agree
|
||||||
|
v-model="agreeConf.checked"
|
||||||
|
:disabled="agreeConf.disabled"
|
||||||
|
:size="agreeConf.size"
|
||||||
|
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||||
|
>
|
||||||
|
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||||
|
</md-agree>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Agree} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'agree-demo',
|
||||||
|
title: '未选中状态',
|
||||||
|
height: 120,
|
||||||
|
components: {
|
||||||
|
[Agree.name]: Agree,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
agreeConf: {
|
||||||
|
checked: false,
|
||||||
|
name: 'agree1',
|
||||||
|
size: 'lg',
|
||||||
|
disabled: false,
|
||||||
|
introduction: '未选中状态',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(name, checked) {
|
||||||
|
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-agree md-example-child-2">
|
||||||
|
<md-agree
|
||||||
|
v-model="agreeConf.checked"
|
||||||
|
:disabled="agreeConf.disabled"
|
||||||
|
:size="agreeConf.size"
|
||||||
|
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||||
|
>
|
||||||
|
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||||
|
</md-agree>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Agree} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'agree-demo',
|
||||||
|
title: '选中不可用状态',
|
||||||
|
height: 120,
|
||||||
|
components: {
|
||||||
|
[Agree.name]: Agree,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
agreeConf: {
|
||||||
|
checked: true,
|
||||||
|
name: 'agree2',
|
||||||
|
size: 'lg',
|
||||||
|
disabled: true,
|
||||||
|
introduction: '选中不可用状态',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(name, checked) {
|
||||||
|
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-agree md-example-child-3">
|
||||||
|
<md-agree
|
||||||
|
v-model="agreeConf.checked"
|
||||||
|
:disabled="agreeConf.disabled"
|
||||||
|
:size="agreeConf.size"
|
||||||
|
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||||
|
>
|
||||||
|
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||||
|
</md-agree>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Agree} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'agree-demo',
|
||||||
|
title: '未选中不可用状态',
|
||||||
|
height: 120,
|
||||||
|
components: {
|
||||||
|
[Agree.name]: Agree,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
agreeConf: {
|
||||||
|
checked: false,
|
||||||
|
name: 'agree3',
|
||||||
|
size: 'lg',
|
||||||
|
disabled: true,
|
||||||
|
introduction: '未选中不可用状态',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(name, checked) {
|
||||||
|
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example agree">
|
||||||
|
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||||
|
<div class="md-example-title" v-html="demo.title"></div>
|
||||||
|
<div class="md-example-content">
|
||||||
|
<component :is="demo"></component>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||||
|
import Demo0 from './cases/demo0'
|
||||||
|
import Demo1 from './cases/demo1'
|
||||||
|
import Demo2 from './cases/demo2'
|
||||||
|
import Demo3 from './cases/demo3'
|
||||||
|
|
||||||
|
export default {...createDemoModule('action-sheet', [Demo0, Demo1, Demo2, Demo3])}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-example.agree
|
||||||
|
.md-example-child
|
||||||
|
padding v-gap-md h-gap-lg
|
||||||
|
font-size font-minor-large
|
||||||
|
background color-bg-base
|
||||||
|
</style>
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="md-agree"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'disabled' : ''
|
||||||
|
]">
|
||||||
|
<div
|
||||||
|
class="agree-icon"
|
||||||
|
:class="[
|
||||||
|
value ? 'checked' : ''
|
||||||
|
]"
|
||||||
|
@click="$_onChange($event)">
|
||||||
|
<md-icon
|
||||||
|
:name="iconName"
|
||||||
|
:size="size"></md-icon>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import Icon from '../icon'
|
||||||
|
export default {
|
||||||
|
name: 'md-agree',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
[Icon.name]: Icon,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'md',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
iconName() {
|
||||||
|
return this.value ? 'circle-right' : 'circle'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// MARK: events handler, 如 $_onButtonClick
|
||||||
|
$_onChange(event) {
|
||||||
|
if (this.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit('input', !this.value)
|
||||||
|
this.$emit('change', event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-agree
|
||||||
|
display flex
|
||||||
|
&.disabled
|
||||||
|
opacity opacity-disabled
|
||||||
|
.agree-icon
|
||||||
|
margin-right 10px
|
||||||
|
color agree-fill-inverse
|
||||||
|
&.checked
|
||||||
|
color agree-fill
|
||||||
|
</style>
|
|
@ -0,0 +1,49 @@
|
||||||
|
import Agree from '../index'
|
||||||
|
import {mount} from 'avoriaz'
|
||||||
|
|
||||||
|
describe('Agree', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a simple agree', () => {
|
||||||
|
wrapper = mount(Agree)
|
||||||
|
expect(wrapper.hasClass('md-agree')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a simple checked agree and then uncheck it', () => {
|
||||||
|
wrapper = mount(Agree, {
|
||||||
|
propsData: {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.iconName).to.equal('circle-right')
|
||||||
|
|
||||||
|
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||||
|
wrapper.find('.agree-icon')[0].trigger('click')
|
||||||
|
expect(eventStub.calledWith('change')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a simple unchecked agree and then check it', () => {
|
||||||
|
wrapper = mount(Agree, {
|
||||||
|
propsData: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.iconName).to.equal('circle')
|
||||||
|
|
||||||
|
wrapper.vm.value = true
|
||||||
|
expect(wrapper.vm.iconName).to.equal('circle-right')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a disabled agree', () => {
|
||||||
|
wrapper = mount(Agree, {
|
||||||
|
propsData: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(wrapper.hasClass('disabled')).to.be.true
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
title: Button 按钮
|
||||||
|
preview: https://didi.github.io/mand-mobile/examples/button
|
||||||
|
---
|
||||||
|
|
||||||
|
按钮组件,可配置多种不同的按钮样式
|
||||||
|
|
||||||
|
### 引入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Button } from 'mand-mobile'
|
||||||
|
|
||||||
|
Vue.component(Button.name, Button)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
<!-- DEMO -->
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### Button Props
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注 |
|
||||||
|
|----|-----|------|------ |------|
|
||||||
|
|type|按钮类型|String|`primary`|`primary`, `ghost`, `ghost-primary`, `link`|
|
||||||
|
|size|按钮大小|String|`large`|`large`, `small`。仅在`type`为`ghost/ghost-primary`时生效|
|
||||||
|
|icon|按钮图标|String|-|可选值请参考组件`Icon`|
|
||||||
|
|disabled|是否禁用|Boolean|`false`|-|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-button md-example-child-button-0">
|
||||||
|
<md-button>Primary</md-button>
|
||||||
|
<md-button disabled>Primary Disabled</md-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Button} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'button-demo',
|
||||||
|
title: '主按钮',
|
||||||
|
components: {
|
||||||
|
[Button.name]: Button,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-button md-example-child-button-1">
|
||||||
|
<md-button type="ghost">Ghost</md-button>
|
||||||
|
<md-button type="ghost" disabled style="margin-left:5px">Ghost</md-button>
|
||||||
|
<md-button type="ghost-primary" style="margin-left:5px">Ghost-P</md-button>
|
||||||
|
<md-button type="ghost-primary" disabled style="margin-left:5px">Ghost-P</md-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Button} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'button-demo',
|
||||||
|
title: '线性按钮',
|
||||||
|
components: {
|
||||||
|
[Button.name]: Button,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-button md-example-child-button-2">
|
||||||
|
<md-button type="ghost" size="small">Ghost-S</md-button>
|
||||||
|
<md-button type="ghost" size="small" disabled style="margin-left:5px">Ghost-S</md-button>
|
||||||
|
<md-button type="ghost-primary" size="small" style="margin-left:5px">Ghost-P-S</md-button>
|
||||||
|
<md-button type="ghost-primary" size="small" disabled style="margin-left:5px">Ghost-P-S</md-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Button} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'button-demo',
|
||||||
|
title: '线性按钮小尺寸',
|
||||||
|
components: {
|
||||||
|
[Button.name]: Button,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-button md-example-child-button-3">
|
||||||
|
<div class="md-example-box-content">每个人都有属于自己的一片森林,也许我们从来不曾去过,但它一直在那里,总会在那里。迷失的人迷失了,相逢的人会再相逢。</div>
|
||||||
|
<md-button type="link">阅读全文</md-button>
|
||||||
|
<div class="md-example-box-content" style="margin-top:10px">希望你可以记住我,记住我这样活过,这样在你身边待过。</div>
|
||||||
|
<md-button type="link" icon="hollow-plus">加入收藏</md-button>
|
||||||
|
<div class="md-example-box-content" style="margin-top:10px">少年时我们追求激情,成熟后却迷恋平庸。在我们寻找、伤害、背离之后,还能一如既往地相信爱情,这是一种勇气。</div>
|
||||||
|
<md-button type="link" disabled>评论</md-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Button} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'button-demo',
|
||||||
|
title: '文字链接按钮',
|
||||||
|
components: {
|
||||||
|
[Button.name]: Button,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.md-example-child-button-3
|
||||||
|
.md-example-box-content
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
padding 60px h-gap-lg
|
||||||
|
color color-text-base
|
||||||
|
font-size font-minor-large
|
||||||
|
text-align left
|
||||||
|
background color-bg-base
|
||||||
|
box-sizing border-box
|
||||||
|
line-height 1.5
|
||||||
|
text-indent 2em
|
||||||
|
.md-button
|
||||||
|
float left
|
||||||
|
</style>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example button">
|
||||||
|
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||||
|
<div class="md-example-title" v-html="demo.title"></div>
|
||||||
|
<div class="md-example-content">
|
||||||
|
<component :is="demo"></component>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||||
|
import Demo0 from './cases/demo0'
|
||||||
|
import Demo1 from './cases/demo1'
|
||||||
|
import Demo2 from './cases/demo2'
|
||||||
|
import Demo3 from './cases/demo3'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...createDemoModule('button', [Demo0, Demo1, Demo2, Demo3]),
|
||||||
|
}
|
||||||
|
|
||||||
|
// export default {
|
||||||
|
// name: `button-demo`,
|
||||||
|
// data() {
|
||||||
|
// return {
|
||||||
|
// demos: [Demo0, Demo1, Demo2, Demo3],
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-example.button
|
||||||
|
.md-example-section
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
margin-bottom 10px
|
||||||
|
h1
|
||||||
|
margin-bottom 10px
|
||||||
|
color color-gray-1
|
||||||
|
font-size font-heading-normal
|
||||||
|
font-weight normal
|
||||||
|
.md-button
|
||||||
|
float left
|
||||||
|
margin-bottom 10px
|
||||||
|
.md-example-box
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
hairline(top)
|
||||||
|
.md-example-box-content
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
padding 60px h-gap-lg
|
||||||
|
color color-text-base
|
||||||
|
font-size font-minor-large
|
||||||
|
text-align left
|
||||||
|
background color-bg-base
|
||||||
|
box-sizing border-box
|
||||||
|
line-height 1.5
|
||||||
|
text-indent 2em
|
||||||
|
.md-button
|
||||||
|
margin 0
|
||||||
|
</style>
|
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="md-button"
|
||||||
|
:class="[type, size, disabled ? 'disabled' : '', icon ? 'with-icon' : '']"
|
||||||
|
@click="$_onBtnClick"
|
||||||
|
>
|
||||||
|
<div class="md-button-inner">
|
||||||
|
<template v-if="icon">
|
||||||
|
<md-icon :name="icon"></md-icon>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import Icon from '../icon'
|
||||||
|
export default {
|
||||||
|
name: 'md-button',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
[Icon.name]: Icon,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'large',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
$_onBtnClick(event) {
|
||||||
|
if (this.disabled) {
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-button
|
||||||
|
-webkit-user-select none
|
||||||
|
-webkit-tap-highlight-color transparent
|
||||||
|
position relative
|
||||||
|
text-align center
|
||||||
|
border-radius radius-normal
|
||||||
|
box-sizing border-box
|
||||||
|
overflow visible
|
||||||
|
// &.disabled
|
||||||
|
// pointer-events none
|
||||||
|
&::before
|
||||||
|
absolute-pos()
|
||||||
|
display none
|
||||||
|
content ''
|
||||||
|
position absolute
|
||||||
|
box-sizing border-box
|
||||||
|
&:active::before
|
||||||
|
display block
|
||||||
|
.md-button-inner
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
overflow hidden
|
||||||
|
text-overflow ellipsis
|
||||||
|
word-break break-word
|
||||||
|
white-space nowrap
|
||||||
|
|
||||||
|
// type
|
||||||
|
&.primary
|
||||||
|
background-color button-primary-fill
|
||||||
|
color color-text-base-inverse
|
||||||
|
&:active::before
|
||||||
|
background-color button-primary-fill-tap
|
||||||
|
&.disabled
|
||||||
|
background-color button-primary-fill-disabled
|
||||||
|
&.large, &.small
|
||||||
|
width button-primary-width
|
||||||
|
height button-primary-height
|
||||||
|
line-height button-primary-height
|
||||||
|
font-size button-primary-font-size
|
||||||
|
font-weight font-weight-medium
|
||||||
|
|
||||||
|
&.ghost
|
||||||
|
color button-ghost-color
|
||||||
|
hairline(all, button-ghost-color, true)
|
||||||
|
&:active::before
|
||||||
|
background-color button-ghost-fill-tap
|
||||||
|
&.ghost-primary
|
||||||
|
color button-ghost-primary-color
|
||||||
|
hairline(all, button-ghost-primary-color, true)
|
||||||
|
&:active::before
|
||||||
|
background-color button-ghost-primary-fill-tap
|
||||||
|
&.ghost, &.ghost-primary
|
||||||
|
&.disabled
|
||||||
|
opacity opacity-disabled
|
||||||
|
&.large
|
||||||
|
width button-ghost-width
|
||||||
|
height button-ghost-height
|
||||||
|
line-height button-ghost-height
|
||||||
|
font-size button-ghost-font-size
|
||||||
|
&.small
|
||||||
|
width button-ghost-width-sm
|
||||||
|
height button-ghost-height-sm
|
||||||
|
line-height button-ghost-height-sm
|
||||||
|
font-size button-ghost-font-size
|
||||||
|
|
||||||
|
&.link
|
||||||
|
background-color button-link-fill
|
||||||
|
color button-link-color
|
||||||
|
.md-button-inner
|
||||||
|
hairline(top, color-border-base)
|
||||||
|
hairline(bottom, color-border-base)
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
&:active::before
|
||||||
|
background-color button-link-fill-tap
|
||||||
|
&.disabled
|
||||||
|
opacity opacity-disabled
|
||||||
|
&.large, &.small
|
||||||
|
width button-link-width
|
||||||
|
height button-link-height
|
||||||
|
font-size font-heading-normal
|
||||||
|
&.with-icon
|
||||||
|
.md-icon
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
margin-right h-gap-sm
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,96 @@
|
||||||
|
import Button from '../index'
|
||||||
|
import {mount} from 'avoriaz'
|
||||||
|
|
||||||
|
describe('Button', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create default button', () => {
|
||||||
|
wrapper = mount(Button)
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('md-button') && wrapper.hasClass('primary') && wrapper.hasClass('large')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create primary button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('primary')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create ghost button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
type: 'ghost',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('ghost')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create ghost-primary button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
type: 'ghost-primary',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('ghost-primary')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create link button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
type: 'link',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('link')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create disabled button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('disabled')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create icon button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
icon: 'hollow-plus',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('with-icon')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create large button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
size: 'large',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('large')).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create small button', () => {
|
||||||
|
wrapper = mount(Button, {
|
||||||
|
propsData: {
|
||||||
|
size: 'small',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('small')).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
title: Captcha 验证码
|
||||||
|
preview: https://didi.github.io/mand-mobile/examples/captcha
|
||||||
|
---
|
||||||
|
|
||||||
|
验证码校验窗口
|
||||||
|
|
||||||
|
### 引入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Captcha } from 'mand-mobile'
|
||||||
|
|
||||||
|
Vue.component(Captcha.name, Captcha)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
<!-- DEMO -->
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### Captcha Props
|
||||||
|
|属性 | 说明 | 类型 | 默认值|
|
||||||
|
|----|-----|------|------|
|
||||||
|
|v-model|验证码窗口是否显示|Boolean|`false`|
|
||||||
|
|is-view|是否内嵌在页面内展示,否则以弹层形式|Boolean|`false`|
|
||||||
|
|maxlength|字符最大输入长度, 若为`-1`则不限制输入长度|Number|`4`|
|
||||||
|
|mask|是否掩码|Boolean|`false`|
|
||||||
|
|system|是否使用系统默认键盘|Boolean|`false`|
|
||||||
|
|title|窗口标题|String|-|
|
||||||
|
|appendTo|挂载节点|HTMLElement|`document.body`|
|
||||||
|
|count|倒计时时长, 设置为0的时候不显示倒计时按钮|Number|`60`|
|
||||||
|
|
||||||
|
|
||||||
|
#### Captcha Methods
|
||||||
|
|
||||||
|
#### countdown()
|
||||||
|
开始倒计时
|
||||||
|
|
||||||
|
#### Captcha Events
|
||||||
|
|
||||||
|
##### @show()
|
||||||
|
验证码组件显示事件
|
||||||
|
|
||||||
|
#### @hide()
|
||||||
|
验证码组件隐藏事件
|
||||||
|
|
||||||
|
#### @send(countdown)
|
||||||
|
触发发送验证码事件, 会在第一次显示的时候触发, 其余情况则会在点击重发按钮后出发
|
||||||
|
|
||||||
|
#### @submit(code)
|
||||||
|
用户提交输入内容事件
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
'name': 'captcha',
|
||||||
|
'text': '验证码窗口',
|
||||||
|
'category': 'business',
|
||||||
|
'description': '交互式验证码校验弹窗',
|
||||||
|
'author': 'liuxinyumichael'
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-captcha">
|
||||||
|
<md-field title="文案">
|
||||||
|
<md-input-item
|
||||||
|
title="标题"
|
||||||
|
v-model="title"
|
||||||
|
></md-input-item>
|
||||||
|
<md-input-item
|
||||||
|
title="插槽内容"
|
||||||
|
v-model="content"
|
||||||
|
></md-input-item>
|
||||||
|
</md-field>
|
||||||
|
|
||||||
|
<md-field title="配置">
|
||||||
|
|
||||||
|
<md-field-item
|
||||||
|
title="限制验证码长度"
|
||||||
|
customized
|
||||||
|
align="right">
|
||||||
|
<md-switch v-model="limit"></md-switch>
|
||||||
|
</md-field-item>
|
||||||
|
|
||||||
|
<md-input-item
|
||||||
|
title="验证码长度"
|
||||||
|
type="tel"
|
||||||
|
v-model="maxlength"
|
||||||
|
></md-input-item>
|
||||||
|
|
||||||
|
<md-field-item
|
||||||
|
title="采用掩码"
|
||||||
|
customized
|
||||||
|
align="right">
|
||||||
|
<md-switch v-model="mask"></md-switch>
|
||||||
|
</md-field-item>
|
||||||
|
|
||||||
|
<md-field-item
|
||||||
|
title="使用系统键盘"
|
||||||
|
customized
|
||||||
|
align="right">
|
||||||
|
<md-switch v-model="system"></md-switch>
|
||||||
|
</md-field-item>
|
||||||
|
|
||||||
|
</md-field>
|
||||||
|
<md-button @click.native="next">确定</md-button>
|
||||||
|
|
||||||
|
<md-captcha
|
||||||
|
ref="captcha"
|
||||||
|
v-model="show"
|
||||||
|
:title="title"
|
||||||
|
:maxlength="limit ? parseFloat(maxlength) : -1"
|
||||||
|
:system="system"
|
||||||
|
:mask="mask"
|
||||||
|
:appendTo="appendTo"
|
||||||
|
@submit="submit"
|
||||||
|
@show="onShow"
|
||||||
|
@hide="onHide"
|
||||||
|
@send="onSend"
|
||||||
|
>
|
||||||
|
{{content}}
|
||||||
|
</md-captcha>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Button, Toast, Captcha, InputItem, Field, FieldItem, Switch} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'captcha-demo',
|
||||||
|
title: '自定义',
|
||||||
|
height: 650,
|
||||||
|
components: {
|
||||||
|
[Button.name]: Button,
|
||||||
|
[Captcha.name]: Captcha,
|
||||||
|
[InputItem.name]: InputItem,
|
||||||
|
[Field.name]: Field,
|
||||||
|
[FieldItem.name]: FieldItem,
|
||||||
|
[Switch.name]: Switch,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
appendTo: document.querySelector('.doc-demo-box-priview') || document.body,
|
||||||
|
title: '手机验证码',
|
||||||
|
content: '哎哟,不错哟~',
|
||||||
|
limit: true,
|
||||||
|
maxlength: '6',
|
||||||
|
mask: false,
|
||||||
|
system: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
next() {
|
||||||
|
this.show = true
|
||||||
|
},
|
||||||
|
submit(val) {
|
||||||
|
const max = parseFloat(this.maxlength)
|
||||||
|
if (max < 0 || val.length === max) {
|
||||||
|
this.show = false
|
||||||
|
Toast({
|
||||||
|
content: `你输入了${val}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSend() {
|
||||||
|
this.$refs.captcha.countdown()
|
||||||
|
},
|
||||||
|
onShow() {},
|
||||||
|
onHide() {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-example-child-captcha
|
||||||
|
padding 20px
|
||||||
|
.md-field
|
||||||
|
margin-bottom 40px
|
||||||
|
</style>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-example-child md-example-child-captcha-1">
|
||||||
|
<md-captcha
|
||||||
|
:maxlength="4"
|
||||||
|
:isView="true"
|
||||||
|
>
|
||||||
|
验证码已发送至186****5407
|
||||||
|
</md-captcha>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import {Captcha} from 'mand-mobile'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'captcha-demo',
|
||||||
|
title: '内联',
|
||||||
|
height: 650,
|
||||||
|
components: {
|
||||||
|
[Captcha.name]: Captcha,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-example-child-captcha-1
|
||||||
|
height 650px
|
||||||
|
padding 30px 0
|
||||||
|
.md-number-keyboard
|
||||||
|
margin-top 30px
|
||||||
|
</style>
|
|
@ -0,0 +1,48 @@
|
||||||
|
<<template>
|
||||||
|
<div class="md-example captcha">
|
||||||
|
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||||
|
<div class="md-example-title" v-html="demo.title"></div>
|
||||||
|
<div class="md-example-content">
|
||||||
|
<component :is="demo"></component>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||||
|
import Demo0 from './cases/demo0'
|
||||||
|
// import Demo1 from './cases/demo1'
|
||||||
|
export default {...createDemoModule('captcha', [Demo0])}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-example.captcha
|
||||||
|
.md-example-child
|
||||||
|
padding 0
|
||||||
|
.md-example-section
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
margin-bottom 10px
|
||||||
|
h1
|
||||||
|
margin-bottom 10px
|
||||||
|
color color-gray-1
|
||||||
|
font-size font-heading-normal
|
||||||
|
font-weight normal
|
||||||
|
.md-example-box
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
hairline(top)
|
||||||
|
.md-example-box-content
|
||||||
|
float left
|
||||||
|
width 100%
|
||||||
|
padding 60px h-gap-lg
|
||||||
|
color color-text-base
|
||||||
|
font-size font-minor-large
|
||||||
|
text-align left
|
||||||
|
background color-bg-base
|
||||||
|
box-sizing border-box
|
||||||
|
line-height 1.5
|
||||||
|
text-indent 2em
|
||||||
|
.md-button
|
||||||
|
margin 0
|
||||||
|
</style>
|
|
@ -0,0 +1,207 @@
|
||||||
|
<template>
|
||||||
|
<div class="md-captcha" v-show="isView || value || visible">
|
||||||
|
<template v-if="isView">
|
||||||
|
<div class="md-captcha-content">
|
||||||
|
<h2 class="md-captcha-title" v-if="title" v-text="title"></h2>
|
||||||
|
<div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<md-button
|
||||||
|
v-if="count"
|
||||||
|
type="ghost"
|
||||||
|
size="small"
|
||||||
|
v-text="counterText"
|
||||||
|
:disabled="this.isCounting"
|
||||||
|
@click.native="$_onClickResend"
|
||||||
|
></md-button>
|
||||||
|
</div>
|
||||||
|
<md-codebox
|
||||||
|
ref="codebox"
|
||||||
|
v-model="code"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:system="system"
|
||||||
|
:closable="false"
|
||||||
|
:isView="isView"
|
||||||
|
:mask="mask"
|
||||||
|
:autofocus="false"
|
||||||
|
@submit="$_onSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<md-dialog
|
||||||
|
:value="value"
|
||||||
|
:closable="true"
|
||||||
|
:appendTo="false"
|
||||||
|
position="center"
|
||||||
|
@input="$_onInput"
|
||||||
|
@show="$_onShow"
|
||||||
|
@hide="$_onHide"
|
||||||
|
>
|
||||||
|
<div class="md-captcha-content">
|
||||||
|
<h2 class="md-captcha-title" v-if="title" v-text="title"></h2>
|
||||||
|
<div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<md-button
|
||||||
|
v-if="count"
|
||||||
|
type="ghost"
|
||||||
|
size="small"
|
||||||
|
v-text="counterText"
|
||||||
|
:disabled="this.isCounting"
|
||||||
|
@click.native="$_onClickResend"
|
||||||
|
></md-button>
|
||||||
|
</div>
|
||||||
|
<md-codebox
|
||||||
|
ref="codebox"
|
||||||
|
v-model="code"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:system="system"
|
||||||
|
:closable="false"
|
||||||
|
:mask="mask"
|
||||||
|
:autofocus="false"
|
||||||
|
@submit="$_onSubmit"
|
||||||
|
/>
|
||||||
|
</md-dialog>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
import Dialog from '../dialog'
|
||||||
|
import Codebox from '../codebox'
|
||||||
|
import Button from '../button'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'md-captcha',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
[Dialog.name]: Dialog,
|
||||||
|
[Codebox.name]: Codebox,
|
||||||
|
[Button.name]: Button,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxlength: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 4,
|
||||||
|
},
|
||||||
|
mask: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
appendTo: {
|
||||||
|
default: () => document.body,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 60,
|
||||||
|
},
|
||||||
|
isView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
code: '',
|
||||||
|
visible: false,
|
||||||
|
counterText: '发送验证码',
|
||||||
|
isCounting: false,
|
||||||
|
firstShown: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
if (val) {
|
||||||
|
this.code = ''
|
||||||
|
if (!this.firstShown) {
|
||||||
|
this.firstShown = true
|
||||||
|
this.$emit('send', this.countdown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (this.appendTo && !this.isView) {
|
||||||
|
this.appendTo.appendChild(this.$el)
|
||||||
|
}
|
||||||
|
if (this.value) {
|
||||||
|
this.firstShown = true
|
||||||
|
this.$emit('send', this.countdown)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// MARK: events handler, 如 $_onButtonClick
|
||||||
|
$_onInput(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
},
|
||||||
|
$_onShow() {
|
||||||
|
this.visible = true
|
||||||
|
this.$refs.codebox.focus()
|
||||||
|
this.$emit('show')
|
||||||
|
},
|
||||||
|
$_onHide() {
|
||||||
|
this.visible = false
|
||||||
|
this.$refs.codebox.blur()
|
||||||
|
this.$emit('hide')
|
||||||
|
},
|
||||||
|
$_onSubmit(code) {
|
||||||
|
this.$emit('submit', code)
|
||||||
|
this.resetcount()
|
||||||
|
},
|
||||||
|
$_onClickResend() {
|
||||||
|
this.$emit('send', this.countdown)
|
||||||
|
},
|
||||||
|
// MARK: public methods
|
||||||
|
countdown() {
|
||||||
|
clearInterval(this.__counter__)
|
||||||
|
let i = this.count - 1
|
||||||
|
this.isCounting = true
|
||||||
|
this.counterText = `${i}s后重发`
|
||||||
|
this.__counter__ = setInterval(() => {
|
||||||
|
if (i === 0) {
|
||||||
|
this.resetcount()
|
||||||
|
} else {
|
||||||
|
i--
|
||||||
|
this.counterText = `${i}s后重发`
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
resetcount() {
|
||||||
|
this.isCounting = false
|
||||||
|
this.counterText = '发送验证码'
|
||||||
|
clearInterval(this.__counter__)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus">
|
||||||
|
.md-captcha
|
||||||
|
.md-dialog .md-dialog-content
|
||||||
|
margin-bottom number-keyboard-height
|
||||||
|
.md-captcha-content
|
||||||
|
text-align center
|
||||||
|
margin-bottom 20px
|
||||||
|
font-size 24px
|
||||||
|
.md-captcha-title
|
||||||
|
color color-text-base
|
||||||
|
font-size 32px
|
||||||
|
margin-bottom 15px
|
||||||
|
.md-button
|
||||||
|
margin 0.34rem auto
|
||||||
|
</style>
|
|
@ -0,0 +1,74 @@
|
||||||
|
import Captcha from '../index'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import {mount} from 'avoriaz'
|
||||||
|
|
||||||
|
describe('Captcha', () => {
|
||||||
|
let wrapper
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper && wrapper.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a simple captcha', () => {
|
||||||
|
wrapper = mount(Captcha, {
|
||||||
|
propsData: {
|
||||||
|
system: true,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.hasClass('md-captcha')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create a captcha and not append to body', () => {
|
||||||
|
wrapper = mount(Captcha, {
|
||||||
|
propsData: {
|
||||||
|
appendTo: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.vm.$el.parentNode).not.to.equal(document.body)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clean code after shown again', () => {
|
||||||
|
wrapper = mount(Captcha, {
|
||||||
|
propsData: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
code: '123',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper.setProps({
|
||||||
|
value: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.vm.code).to.equal('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emit submit events', done => {
|
||||||
|
wrapper = mount(Captcha, {
|
||||||
|
propsData: {
|
||||||
|
maxlength: 4,
|
||||||
|
value: false,
|
||||||
|
isView: true,
|
||||||
|
appendTo: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||||
|
wrapper.setProps({
|
||||||
|
value: true,
|
||||||
|
})
|
||||||
|
wrapper.setData({
|
||||||
|
code: '123',
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.first('.keyboard-number-item').trigger('click')
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(eventStub.calledWith('submit', '1231')).to.be.true
|
||||||
|
done()
|
||||||
|
}, 0)
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
title: Cashier 收银台
|
||||||
|
preview: https://didi.github.io/mand-mobile/examples/cashier
|
||||||
|
---
|
||||||
|
|
||||||
|
业务支付弹窗,支持支付渠道选择和支付验证码发送
|
||||||
|
|
||||||
|
### 引入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Cashier } from 'mand-mobile'
|
||||||
|
|
||||||
|
Vue.component(Cashier.name, Cashier)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码演示
|
||||||
|
<!-- DEMO -->
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
#### Cashier Props
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||||
|
|----|-----|------|------|------|
|
||||||
|
|v-model|收银台是否显示|Boolean|`false`|-|
|
||||||
|
|channels|支付渠道数据源|Array<{text, value, icon}>|`[]`|`icon`可作为`className`或组件`Icon`的`name`属性|
|
||||||
|
|default-index|默认选中支付渠道索引|Number|`0`|-|
|
||||||
|
|title|收银台弹窗标题|String|`支付`|-|
|
||||||
|
|payment-title|支付金额标题|String|`支付金额`|支持`html fragment`|
|
||||||
|
|payment-amount|支付金额|String|`0.00`|支持`html fragment`|
|
||||||
|
|payment-describe|支付金额说明|String|-|支持`html fragment`|
|
||||||
|
|
||||||
|
#### Cashier Methods
|
||||||
|
|
||||||
|
##### next(scene, option)
|
||||||
|
进入收银台下一步
|
||||||
|
|
||||||
|
|参数 | 说明 | 类型 | 默认值|
|
||||||
|
|----|-----|------|------|
|
||||||
|
| scene | 步骤标识, 'captcha(发送验证码)', 'loading(支付中)', 'success(支付成功)', 'fail(支付失败)' | String |-|
|
||||||
|
| option | 当前步骤配置 | Object |属性如下所示|
|
||||||
|
|
||||||
|
* `captcha` option
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||||
|
|----|-----|------|------|------|
|
||||||
|
|text|发送验证码说明 | String |-|支持`html fragment`|
|
||||||
|
|maxlength|验证码位数 | Number |`4`|若为`-1`则不限制输入长度|
|
||||||
|
|count|验证码重新发送倒计时 | Number |`60`|若为`0`则不显示重新发送|
|
||||||
|
|onSend|验证码发送回调 | Function(countdown: Function) |-|`countdown`为开始倒计时方法|
|
||||||
|
|onSubmit|验证码提交回调 | Function(code: String) |-|`code`为输入的验证码|
|
||||||
|
|
||||||
|
* `loading` option
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||||
|
|----|-----|------|------|------|
|
||||||
|
|text|支付中说明 | String |`支付结果查询中...`|支持`html fragment`|
|
||||||
|
|
||||||
|
* `success` option
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||||
|
|----|-----|------|------|------|
|
||||||
|
|text|支付成功说明 | String |`支付成功`|支持`html fragment`|
|
||||||
|
|
||||||
|
* `fail` option
|
||||||
|
|
||||||
|
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||||
|
|----|-----|------|------|------|
|
||||||
|
|text|支付失败说明 | String |`支付失败,请稍后重试`|支持`html fragment`|
|
||||||
|
|
||||||
|
#### Cashier Events
|
||||||
|
|
||||||
|
##### @select(item: {text, value})
|
||||||
|
支付渠道选中事件
|
||||||
|
|
||||||
|
##### @pay(item: {text, value})
|
||||||
|
支付渠道确认并发起支付事件
|
||||||
|
|
||||||
|
##### @cancel()
|
||||||
|
取消支付事件
|
||||||
|
|
||||||
|
##### @show()
|
||||||
|
收银台弹窗展示事件
|
||||||
|
|
||||||
|
##### @hide()
|
||||||
|
收银台弹窗隐藏事件
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
"name": "cashier",
|
||||||
|
"text": "收银台",
|
||||||
|
"category": "business",
|
||||||
|
"description": "",
|
||||||
|
"author": "xuxiaoyan"
|
||||||
|
}
|