From 36c6454b797a68acd58ba7f4bf370d7332deaed5 Mon Sep 17 00:00:00 2001 From: moyus Date: Mon, 26 Mar 2018 16:04:04 +0800 Subject: [PATCH] Initial commit --- .babelrc | 19 + .editorconfig | 9 + .eslintignore | 11 + .eslintrc.js | 6 + .gitignore | 24 + CHANGELOG.md | 80 ++ CONTRIBUTING.md | 28 + LICENSE | 433 ++++++ README.md | 141 ++ assets/examples-qrcode.png | Bin 0 -> 643 bytes assets/logo.png | Bin 0 -> 3054 bytes build/check-versions.js | 49 + build/component-init.js | 196 +++ build/mand-change-log.js | 86 ++ build/rollup/build-component.rollup.js | 146 ++ build/rollup/build-example.rollup.js | 28 + build/rollup/build-mand-mobile.rollup.js | 43 + build/rollup/dev-server.rollup.js | 65 + build/rollup/rollup-plugin-config.js | 159 +++ build/stylus-mixin.js | 8 + build/template.exp | 40 + build/webpack/build-example.js | 43 + build/webpack/build-mand-mobile.js | 28 + build/webpack/build-style-entry.js | 67 + build/webpack/dev-client.js | 10 + build/webpack/dev-server.js | 107 ++ build/webpack/utils.js | 84 ++ build/webpack/vue-loader.conf.js | 19 + build/webpack/webpack.base.conf.js | 57 + build/webpack/webpack.build.conf.js | 106 ++ build/webpack/webpack.dev.conf.js | 58 + build/webpack/webpack.example.conf.js | 142 ++ build/webpack/webpack.test.conf.js | 32 + components/_style/global.styl | 7 + components/_style/images/arrow-down.svg | 1 + components/_style/images/arrow-left.svg | 1 + components/_style/images/arrow-right.svg | 1 + components/_style/images/arrow-up.svg | 1 + components/_style/images/circle-alert.svg | 1 + components/_style/images/circle-cross.svg | 1 + components/_style/images/circle-right.svg | 1 + components/_style/images/circle.svg | 1 + components/_style/images/cross.svg | 1 + components/_style/images/hollow-plus.svg | 1 + .../_style/images/keyboard-del-simple.png | Bin 0 -> 543 bytes components/_style/images/keyboard-del.png | Bin 0 -> 947 bytes components/_style/images/keyboard-hide.png | Bin 0 -> 493 bytes components/_style/images/right.svg | 1 + components/_style/images/spinner.svg | 1 + components/_style/mixin/theme.styl | 310 ++++ components/_style/mixin/util.styl | 105 ++ components/_util/animate.js | 204 +++ components/_util/debug.js | 5 + components/_util/env.js | 8 + components/_util/formate-value.js | 76 + components/_util/index.js | 4 + components/_util/lang.js | 71 + components/_util/render.js | 42 + components/_util/scroller.js | 915 ++++++++++++ components/_util/store.js | 128 ++ components/action-bar/README.md | 36 + components/action-bar/component.js | 7 + components/action-bar/demo/cases/demo0.vue | 36 + components/action-bar/demo/cases/demo1.vue | 40 + components/action-bar/demo/cases/demo2.vue | 35 + components/action-bar/demo/index.vue | 27 + components/action-bar/index.vue | 117 ++ components/action-bar/test/index.spec.js | 65 + components/action-sheet/README.md | 47 + components/action-sheet/component.js | 7 + components/action-sheet/demo/cases/demo0.vue | 67 + components/action-sheet/demo/index.vue | 18 + components/action-sheet/index.vue | 146 ++ components/action-sheet/test/index.spec.js | 128 ++ components/agree/README.md | 36 + components/agree/component.js | 7 + components/agree/demo/cases/demo0.vue | 40 + components/agree/demo/cases/demo1.vue | 40 + components/agree/demo/cases/demo2.vue | 40 + components/agree/demo/cases/demo3.vue | 40 + components/agree/demo/index.vue | 27 + components/agree/index.vue | 77 + components/agree/test/index.spec.js | 49 + components/button/README.md | 27 + components/button/demo/cases/demo0.vue | 18 + components/button/demo/cases/demo1.vue | 20 + components/button/demo/cases/demo2.vue | 20 + components/button/demo/cases/demo3.vue | 38 + components/button/demo/index.vue | 63 + components/button/index.vue | 143 ++ components/button/test/index.spec.js | 96 ++ components/captcha/README.md | 51 + components/captcha/component.js | 7 + components/captcha/demo/cases/demo0.vue | 117 ++ components/captcha/demo/cases/demo1.vue | 30 + components/captcha/demo/index.vue | 48 + components/captcha/index.vue | 207 +++ components/captcha/test/index.spec.js | 74 + components/cashier/README.md | 85 ++ components/cashier/component.js | 7 + components/cashier/demo/cases/demo0.vue | 194 +++ components/cashier/demo/index.vue | 22 + components/cashier/index.vue | 421 ++++++ components/cashier/rolling.vue | 122 ++ components/cashier/test/index.spec.js | 156 ++ components/chart/README.md | 69 + components/chart/component.js | 7 + components/chart/demo/cases/demo0.vue | 49 + components/chart/demo/cases/demo1.vue | 46 + components/chart/demo/cases/demo2.vue | 46 + components/chart/demo/index.vue | 24 + components/chart/index.vue | 297 ++++ components/chart/test/index.spec.js | 92 ++ components/codebox/README.md | 44 + components/codebox/component.js | 7 + components/codebox/demo/cases/demo0.vue | 25 + components/codebox/demo/cases/demo1.vue | 25 + components/codebox/demo/cases/demo2.vue | 24 + components/codebox/demo/cases/demo3.vue | 25 + components/codebox/demo/index.vue | 23 + components/codebox/index.vue | 286 ++++ components/codebox/test/index.spec.js | 113 ++ components/date-picker/README.md | 173 +++ components/date-picker/component.js | 7 + components/date-picker/demo/cases/demo0.vue | 37 + components/date-picker/demo/cases/demo1.vue | 23 + components/date-picker/demo/cases/demo2.vue | 34 + components/date-picker/demo/cases/demo3.vue | 76 + components/date-picker/demo/index.vue | 20 + components/date-picker/index.vue | 531 +++++++ components/date-picker/test/index.spec.js | 93 ++ components/dialog/README.md | 93 ++ components/dialog/component.js | 7 + components/dialog/demo/cases/demo0.vue | 108 ++ components/dialog/demo/cases/demo1.vue | 55 + components/dialog/demo/index.vue | 23 + components/dialog/dialog.vue | 189 +++ components/dialog/index.js | 86 ++ components/dialog/test/index.spec.js | 103 ++ components/drop-menu/README.md | 66 + components/drop-menu/component.js | 7 + components/drop-menu/demo/cases/demo0.vue | 52 + components/drop-menu/demo/cases/demo1.vue | 66 + components/drop-menu/demo/cases/demo2.vue | 57 + components/drop-menu/demo/cases/demo3.vue | 61 + components/drop-menu/demo/index.vue | 43 + components/drop-menu/index.vue | 236 +++ components/drop-menu/test/index.spec.js | 193 +++ components/field-item/index.vue | 170 +++ components/field-item/test/index.spec.js | 87 ++ components/field/README.md | 47 + components/field/component.js | 7 + components/field/demo/cases/demo0.vue | 62 + components/field/demo/cases/demo1.vue | 72 + components/field/demo/cases/demo2.vue | 61 + components/field/demo/index.vue | 18 + components/field/index.vue | 50 + components/field/test/index.spec.js | 29 + components/icon/README.md | 82 ++ components/icon/default-svg-list.js | 26 + components/icon/demo/cases/demo0.vue | 59 + components/icon/demo/cases/demo1.vue | 31 + components/icon/demo/cases/demo2.vue | 31 + components/icon/demo/index.vue | 35 + components/icon/index.vue | 57 + components/icon/load-spirte.js | 40 + components/icon/test/index.spec.js | 17 + components/image-reader/README.md | 71 + components/image-reader/component.js | 7 + components/image-reader/demo/cases/demo0.vue | 123 ++ components/image-reader/demo/cases/demo1.vue | 132 ++ components/image-reader/demo/index.vue | 20 + components/image-reader/image-dataurl.js | 50 + components/image-reader/image-processor.js | 222 +++ components/image-reader/image-reader.js | 91 ++ components/image-reader/index.vue | 185 +++ components/image-reader/test/file.mock.js | 18 + components/image-reader/test/index.spec.js | 54 + components/image-viewer/README.md | 29 + components/image-viewer/component.js | 7 + components/image-viewer/demo/cases/demo0.vue | 76 + components/image-viewer/demo/index.vue | 19 + components/image-viewer/index.vue | 173 +++ components/image-viewer/test/index.spec.js | 39 + components/index.js | 154 ++ components/input-item/README.md | 81 ++ components/input-item/component.js | 7 + components/input-item/cursor.js | 44 + components/input-item/demo/cases/demo0.vue | 60 + components/input-item/demo/cases/demo1.vue | 32 + components/input-item/demo/cases/demo2.vue | 56 + components/input-item/demo/cases/demo3.vue | 102 ++ components/input-item/demo/cases/demo4.vue | 33 + components/input-item/demo/index.vue | 20 + components/input-item/index.vue | 657 +++++++++ components/input-item/keycode.js | 32 + components/input-item/test/index.spec.js | 139 ++ components/landscape/README.md | 34 + components/landscape/component.js | 7 + components/landscape/demo/cases/demo0.vue | 67 + components/landscape/demo/index.vue | 17 + components/landscape/index.vue | 99 ++ components/landscape/test/index.spec.js | 49 + components/notice-bar/README.md | 28 + components/notice-bar/component.js | 7 + components/notice-bar/demo/cases/demo0.vue | 16 + components/notice-bar/demo/cases/demo1.vue | 21 + components/notice-bar/demo/cases/demo2.vue | 17 + components/notice-bar/demo/index.vue | 19 + components/notice-bar/index.vue | 79 ++ components/notice-bar/test/index.spec.js | 46 + components/number-keyboard/README.md | 51 + components/number-keyboard/component.js | 7 + .../number-keyboard/demo/cases/demo0.vue | 52 + .../number-keyboard/demo/cases/demo1.vue | 54 + .../number-keyboard/demo/cases/demo2.vue | 53 + components/number-keyboard/demo/index.vue | 18 + components/number-keyboard/index.vue | 119 ++ components/number-keyboard/keyboard.vue | 187 +++ components/number-keyboard/test/index.spec.js | 56 + components/picker/README.md | 172 +++ components/picker/cascade.js | 37 + components/picker/component.js | 7 + components/picker/demo/cases/demo0.vue | 63 + components/picker/demo/cases/demo1.vue | 74 + components/picker/demo/cases/demo2.vue | 54 + components/picker/demo/data/district.js | 1261 +++++++++++++++++ components/picker/demo/data/simple.js | 20 + components/picker/demo/index.vue | 19 + components/picker/index.vue | 285 ++++ components/picker/picker-column.vue | 478 +++++++ components/picker/test/index.spec.js | 95 ++ components/popup-title-bar/index.vue | 4 + components/popup/README.md | 60 + components/popup/component.js | 7 + components/popup/demo/cases/demo0.vue | 128 ++ components/popup/demo/cases/demo1.vue | 124 ++ components/popup/demo/index.vue | 18 + components/popup/index.vue | 275 ++++ components/popup/test/index.spec.js | 107 ++ components/popup/test/touch-trigger.js | 23 + components/popup/title-bar.vue | 107 ++ components/radio/README.md | 89 ++ components/radio/component.js | 7 + components/radio/demo/cases/demo0.vue | 48 + components/radio/demo/cases/demo1.vue | 48 + components/radio/demo/cases/demo2.vue | 39 + components/radio/demo/cases/demo3.vue | 40 + components/radio/demo/cases/demo4.vue | 29 + components/radio/demo/index.vue | 21 + components/radio/index.vue | 293 ++++ components/radio/test/index.spec.js | 95 ++ components/result-page/README.md | 39 + components/result-page/component.js | 7 + components/result-page/demo/cases/demo0.vue | 24 + components/result-page/demo/cases/demo1.vue | 25 + components/result-page/demo/cases/demo2.vue | 43 + components/result-page/demo/cases/demo3.vue | 28 + components/result-page/demo/index.vue | 26 + components/result-page/index.vue | 105 ++ components/result-page/test/index.spec.js | 16 + components/selector/README.md | 51 + components/selector/component.js | 7 + components/selector/demo/cases/demo0.vue | 67 + components/selector/demo/cases/demo1.vue | 80 ++ components/selector/demo/cases/demo2.vue | 67 + components/selector/demo/cases/demo3.vue | 68 + components/selector/demo/index.vue | 30 + components/selector/index.vue | 229 +++ components/selector/test/index.spec.js | 180 +++ components/stepper/README.md | 34 + components/stepper/component.js | 7 + components/stepper/demo/cases/demo0.vue | 30 + components/stepper/demo/cases/demo1.vue | 30 + components/stepper/demo/cases/demo2.vue | 31 + components/stepper/demo/cases/demo3.vue | 31 + components/stepper/demo/cases/demo4.vue | 30 + components/stepper/demo/cases/demo5.vue | 30 + components/stepper/demo/index.vue | 28 + components/stepper/index.vue | 231 +++ components/stepper/test/index.spec.js | 111 ++ components/steps/README.md | 25 + components/steps/component.js | 7 + components/steps/demo/cases/demo0.vue | 34 + components/steps/demo/cases/demo1.vue | 37 + components/steps/demo/cases/demo2.vue | 42 + components/steps/demo/cases/demo3.vue | 38 + components/steps/demo/cases/demo4.vue | 44 + components/steps/demo/cases/demo5.vue | 52 + components/steps/demo/cases/demo6.vue | 52 + components/steps/demo/index.vue | 28 + components/steps/index.vue | 138 ++ components/steps/test/index.spec.js | 16 + components/swiper-item/index.vue | 4 + components/swiper/README.md | 104 ++ components/swiper/component.js | 7 + components/swiper/demo/cases/demo0.vue | 89 ++ components/swiper/demo/cases/demo1.vue | 70 + components/swiper/demo/cases/demo2.vue | 75 + components/swiper/demo/cases/demo3.vue | 81 ++ components/swiper/demo/data/mulit-item.js | 82 ++ components/swiper/demo/data/simple.js | 16 + components/swiper/demo/index.vue | 35 + components/swiper/index.vue | 675 +++++++++ components/swiper/swiper-item.vue | 68 + components/swiper/test/index.spec.js | 392 +++++ components/switch/README.md | 34 + components/switch/component.js | 7 + components/switch/demo/cases/demo0.vue | 29 + components/switch/demo/cases/demo1.vue | 29 + components/switch/demo/cases/demo2.vue | 30 + components/switch/demo/cases/demo3.vue | 30 + components/switch/demo/index.vue | 24 + components/switch/index.vue | 76 + components/switch/test/index.spec.js | 52 + components/tab-bar/README.md | 47 + components/tab-bar/component.js | 7 + components/tab-bar/demo/cases/demo0.vue | 23 + components/tab-bar/demo/cases/demo1.vue | 24 + components/tab-bar/demo/cases/demo2.vue | 26 + components/tab-bar/demo/cases/demo3.vue | 25 + components/tab-bar/demo/cases/demo4.vue | 25 + components/tab-bar/demo/cases/demo5.vue | 31 + components/tab-bar/demo/cases/demo6.vue | 29 + components/tab-bar/demo/cases/demo7.vue | 35 + components/tab-bar/demo/cases/demo8.vue | 43 + components/tab-bar/demo/cases/demo9.vue | 28 + components/tab-bar/demo/index.vue | 30 + components/tab-bar/index.vue | 180 +++ components/tab-bar/test/index.spec.js | 74 + components/tab-picker/README.md | 197 +++ components/tab-picker/component.js | 7 + components/tab-picker/demo/cases/demo0.vue | 55 + components/tab-picker/demo/cases/demo1.vue | 63 + components/tab-picker/demo/cases/demo2.vue | 108 ++ components/tab-picker/demo/data/cascade.js | 46 + components/tab-picker/demo/data/no-cascade.js | 80 ++ components/tab-picker/demo/index.vue | 199 +++ components/tab-picker/index.vue | 389 +++++ components/tab-picker/test/index.spec.js | 159 +++ components/tabs/README.md | 48 + components/tabs/component.js | 7 + components/tabs/demo/cases/demo0.vue | 27 + components/tabs/demo/cases/demo1.vue | 30 + components/tabs/demo/cases/demo10.vue | 47 + components/tabs/demo/cases/demo11.vue | 35 + components/tabs/demo/cases/demo2.vue | 30 + components/tabs/demo/cases/demo3.vue | 31 + components/tabs/demo/cases/demo4.vue | 29 + components/tabs/demo/cases/demo5.vue | 47 + components/tabs/demo/cases/demo6.vue | 34 + components/tabs/demo/cases/demo7.vue | 34 + components/tabs/demo/cases/demo8.vue | 28 + components/tabs/demo/cases/demo9.vue | 28 + components/tabs/demo/index.vue | 38 + components/tabs/index.vue | 194 +++ components/tabs/test/index.spec.js | 82 ++ components/tag/README.md | 29 + components/tag/component.js | 7 + components/tag/demo/cases/demo0.vue | 19 + components/tag/demo/cases/demo1.vue | 18 + components/tag/demo/cases/demo2.vue | 17 + components/tag/demo/cases/demo3.vue | 17 + components/tag/demo/index.vue | 30 + components/tag/index.vue | 121 ++ components/tag/test/index.spec.js | 44 + components/tip/README.md | 29 + components/tip/component.js | 7 + components/tip/demo/cases/demo0.vue | 19 + components/tip/demo/cases/demo1.vue | 19 + components/tip/demo/cases/demo2.vue | 19 + components/tip/demo/cases/demo3.vue | 19 + components/tip/demo/cases/demo4.vue | 31 + components/tip/demo/index.vue | 27 + components/tip/index.js | 195 +++ components/tip/test/index.spec.js | 58 + components/tip/tip.vue | 96 ++ components/toast/README.md | 73 + components/toast/component.js | 7 + components/toast/demo/cases/demo0.vue | 21 + components/toast/demo/cases/demo1.vue | 21 + components/toast/demo/cases/demo2.vue | 21 + components/toast/demo/cases/demo3.vue | 24 + components/toast/demo/cases/demo4.vue | 27 + components/toast/demo/cases/demo5.vue | 21 + components/toast/demo/index.vue | 22 + components/toast/index.js | 116 ++ components/toast/test/index.spec.js | 96 ++ components/toast/toast.vue | 111 ++ config/dev.env.js | 7 + config/index.js | 61 + config/prod.env.js | 4 + config/test.env.js | 7 + examples/App.vue | 147 ++ examples/assets/images/bank-zs.svg | 1 + examples/assets/images/cashier-icon-1.png | Bin 0 -> 343 bytes examples/assets/images/cashier-icon-2.png | Bin 0 -> 549 bytes examples/assets/images/cashier-icon-3.png | Bin 0 -> 451 bytes examples/assets/images/cashier-icon-4.png | Bin 0 -> 929 bytes examples/assets/images/cashier-icon-5.png | Bin 0 -> 720 bytes examples/assets/responsive.js | 26 + examples/category.vue | 187 +++ examples/components.json | 1 + examples/create-demo-module.js | 10 + examples/demo-index.indemand.js | 39 + examples/demo-index.js | 38 + examples/home.indemand.vue | 94 ++ examples/home.vue | 95 ++ examples/index.html | 31 + examples/main.indemand.js | 32 + examples/main.js | 36 + examples/route.indemand.js | 46 + examples/route.js | 44 + examples/single-component-app.vue | 126 ++ examples/single-component-main.js | 16 + examples/theme.custom.styl | 1 + gulpfile.js | 172 +++ package.json | 212 +++ postcss.config.js | 10 + site/.babelrc | 24 + site/.eslintignore | 3 + site/.eslintrc.js | 27 + site/.gitignore | 18 + site/.postcssrc.js | 8 + site/build/bin/default.mfe.blog.config.js | 26 + site/build/bin/gen-indices.js | 84 ++ site/build/bin/markdown.js | 49 + site/build/bin/mfe-blog-dev.js | 47 + site/build/bin/mfe-blog-generate.js | 212 +++ site/build/bin/utils.js | 66 + site/build/build.js | 41 + site/build/check-versions.js | 48 + site/build/dev-client.js | 9 + site/build/dev-server.js | 95 ++ site/build/utils.js | 83 ++ site/build/vue-loader.conf.js | 16 + site/build/webpack.base.conf.js | 71 + site/build/webpack.dev.conf.js | 40 + site/build/webpack.prod.conf.js | 73 + site/config/dev.env.js | 6 + site/config/index.js | 41 + site/config/prod.env.js | 3 + site/index.html | 34 + site/mfe.blog.config.js | 153 ++ site/package.json | 90 ++ site/theme/default/App.vue | 87 ++ site/theme/default/Error.vue | 60 + site/theme/default/Home.vue | 409 ++++++ site/theme/default/Preview.vue | 106 ++ site/theme/default/assets/css/demo.styl | 158 +++ site/theme/default/assets/css/global.styl | 17 + site/theme/default/assets/css/hightlight.css | 17 + site/theme/default/assets/css/markdown.styl | 451 ++++++ site/theme/default/assets/css/mixin.styl | 9 + site/theme/default/assets/css/toc.styl | 46 + site/theme/default/assets/js/home.config.js | 86 ++ site/theme/default/assets/js/responsive.js | 26 + site/theme/default/assets/js/util.js | 34 + site/theme/default/components/Doc.vue | 501 +++++++ site/theme/default/components/Footer.vue | 141 ++ site/theme/default/components/Header.vue | 377 +++++ site/theme/default/components/Menu.vue | 116 ++ site/theme/default/components/Table.vue | 159 +++ site/theme/default/main.js | 19 + site/theme/default/router/index.js | 37 + static/.gitkeep | 0 static/animate.css | 16 + static/jquery.lettering.js | 1 + static/jquery.textillate.js | 1 + static/pace.css | 1 + static/pace.js | 2 + test/unit/.eslintrc | 9 + test/unit/index.js | 8 + test/unit/index.rollup.js | 1 + test/unit/karma.conf.js | 47 + test/unit/rollup.karma.conf.js | 52 + types/component.d.ts | 5 + types/dialog.d.ts | 27 + types/image-processor.d.ts | 15 + types/index.d.ts | 59 + types/toast.d.ts | 19 + 481 files changed, 34110 insertions(+) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/examples-qrcode.png create mode 100644 assets/logo.png create mode 100644 build/check-versions.js create mode 100644 build/component-init.js create mode 100644 build/mand-change-log.js create mode 100644 build/rollup/build-component.rollup.js create mode 100644 build/rollup/build-example.rollup.js create mode 100644 build/rollup/build-mand-mobile.rollup.js create mode 100644 build/rollup/dev-server.rollup.js create mode 100644 build/rollup/rollup-plugin-config.js create mode 100644 build/stylus-mixin.js create mode 100644 build/template.exp create mode 100644 build/webpack/build-example.js create mode 100644 build/webpack/build-mand-mobile.js create mode 100644 build/webpack/build-style-entry.js create mode 100644 build/webpack/dev-client.js create mode 100644 build/webpack/dev-server.js create mode 100644 build/webpack/utils.js create mode 100644 build/webpack/vue-loader.conf.js create mode 100644 build/webpack/webpack.base.conf.js create mode 100644 build/webpack/webpack.build.conf.js create mode 100644 build/webpack/webpack.dev.conf.js create mode 100644 build/webpack/webpack.example.conf.js create mode 100644 build/webpack/webpack.test.conf.js create mode 100644 components/_style/global.styl create mode 100644 components/_style/images/arrow-down.svg create mode 100644 components/_style/images/arrow-left.svg create mode 100644 components/_style/images/arrow-right.svg create mode 100644 components/_style/images/arrow-up.svg create mode 100644 components/_style/images/circle-alert.svg create mode 100644 components/_style/images/circle-cross.svg create mode 100644 components/_style/images/circle-right.svg create mode 100644 components/_style/images/circle.svg create mode 100644 components/_style/images/cross.svg create mode 100644 components/_style/images/hollow-plus.svg create mode 100644 components/_style/images/keyboard-del-simple.png create mode 100644 components/_style/images/keyboard-del.png create mode 100644 components/_style/images/keyboard-hide.png create mode 100644 components/_style/images/right.svg create mode 100644 components/_style/images/spinner.svg create mode 100644 components/_style/mixin/theme.styl create mode 100644 components/_style/mixin/util.styl create mode 100644 components/_util/animate.js create mode 100644 components/_util/debug.js create mode 100644 components/_util/env.js create mode 100644 components/_util/formate-value.js create mode 100644 components/_util/index.js create mode 100644 components/_util/lang.js create mode 100644 components/_util/render.js create mode 100644 components/_util/scroller.js create mode 100644 components/_util/store.js create mode 100644 components/action-bar/README.md create mode 100644 components/action-bar/component.js create mode 100644 components/action-bar/demo/cases/demo0.vue create mode 100644 components/action-bar/demo/cases/demo1.vue create mode 100644 components/action-bar/demo/cases/demo2.vue create mode 100644 components/action-bar/demo/index.vue create mode 100644 components/action-bar/index.vue create mode 100644 components/action-bar/test/index.spec.js create mode 100644 components/action-sheet/README.md create mode 100644 components/action-sheet/component.js create mode 100644 components/action-sheet/demo/cases/demo0.vue create mode 100644 components/action-sheet/demo/index.vue create mode 100644 components/action-sheet/index.vue create mode 100644 components/action-sheet/test/index.spec.js create mode 100644 components/agree/README.md create mode 100644 components/agree/component.js create mode 100644 components/agree/demo/cases/demo0.vue create mode 100644 components/agree/demo/cases/demo1.vue create mode 100644 components/agree/demo/cases/demo2.vue create mode 100644 components/agree/demo/cases/demo3.vue create mode 100644 components/agree/demo/index.vue create mode 100644 components/agree/index.vue create mode 100644 components/agree/test/index.spec.js create mode 100644 components/button/README.md create mode 100644 components/button/demo/cases/demo0.vue create mode 100644 components/button/demo/cases/demo1.vue create mode 100644 components/button/demo/cases/demo2.vue create mode 100644 components/button/demo/cases/demo3.vue create mode 100644 components/button/demo/index.vue create mode 100644 components/button/index.vue create mode 100644 components/button/test/index.spec.js create mode 100644 components/captcha/README.md create mode 100644 components/captcha/component.js create mode 100644 components/captcha/demo/cases/demo0.vue create mode 100644 components/captcha/demo/cases/demo1.vue create mode 100644 components/captcha/demo/index.vue create mode 100644 components/captcha/index.vue create mode 100644 components/captcha/test/index.spec.js create mode 100644 components/cashier/README.md create mode 100644 components/cashier/component.js create mode 100644 components/cashier/demo/cases/demo0.vue create mode 100644 components/cashier/demo/index.vue create mode 100644 components/cashier/index.vue create mode 100644 components/cashier/rolling.vue create mode 100644 components/cashier/test/index.spec.js create mode 100644 components/chart/README.md create mode 100644 components/chart/component.js create mode 100644 components/chart/demo/cases/demo0.vue create mode 100644 components/chart/demo/cases/demo1.vue create mode 100644 components/chart/demo/cases/demo2.vue create mode 100644 components/chart/demo/index.vue create mode 100644 components/chart/index.vue create mode 100644 components/chart/test/index.spec.js create mode 100644 components/codebox/README.md create mode 100644 components/codebox/component.js create mode 100644 components/codebox/demo/cases/demo0.vue create mode 100644 components/codebox/demo/cases/demo1.vue create mode 100644 components/codebox/demo/cases/demo2.vue create mode 100644 components/codebox/demo/cases/demo3.vue create mode 100644 components/codebox/demo/index.vue create mode 100644 components/codebox/index.vue create mode 100644 components/codebox/test/index.spec.js create mode 100644 components/date-picker/README.md create mode 100644 components/date-picker/component.js create mode 100644 components/date-picker/demo/cases/demo0.vue create mode 100644 components/date-picker/demo/cases/demo1.vue create mode 100644 components/date-picker/demo/cases/demo2.vue create mode 100644 components/date-picker/demo/cases/demo3.vue create mode 100644 components/date-picker/demo/index.vue create mode 100644 components/date-picker/index.vue create mode 100644 components/date-picker/test/index.spec.js create mode 100644 components/dialog/README.md create mode 100644 components/dialog/component.js create mode 100644 components/dialog/demo/cases/demo0.vue create mode 100644 components/dialog/demo/cases/demo1.vue create mode 100644 components/dialog/demo/index.vue create mode 100644 components/dialog/dialog.vue create mode 100644 components/dialog/index.js create mode 100644 components/dialog/test/index.spec.js create mode 100644 components/drop-menu/README.md create mode 100644 components/drop-menu/component.js create mode 100644 components/drop-menu/demo/cases/demo0.vue create mode 100644 components/drop-menu/demo/cases/demo1.vue create mode 100644 components/drop-menu/demo/cases/demo2.vue create mode 100644 components/drop-menu/demo/cases/demo3.vue create mode 100644 components/drop-menu/demo/index.vue create mode 100644 components/drop-menu/index.vue create mode 100644 components/drop-menu/test/index.spec.js create mode 100644 components/field-item/index.vue create mode 100644 components/field-item/test/index.spec.js create mode 100644 components/field/README.md create mode 100644 components/field/component.js create mode 100644 components/field/demo/cases/demo0.vue create mode 100644 components/field/demo/cases/demo1.vue create mode 100644 components/field/demo/cases/demo2.vue create mode 100644 components/field/demo/index.vue create mode 100644 components/field/index.vue create mode 100644 components/field/test/index.spec.js create mode 100644 components/icon/README.md create mode 100644 components/icon/default-svg-list.js create mode 100644 components/icon/demo/cases/demo0.vue create mode 100644 components/icon/demo/cases/demo1.vue create mode 100644 components/icon/demo/cases/demo2.vue create mode 100644 components/icon/demo/index.vue create mode 100644 components/icon/index.vue create mode 100644 components/icon/load-spirte.js create mode 100644 components/icon/test/index.spec.js create mode 100644 components/image-reader/README.md create mode 100644 components/image-reader/component.js create mode 100644 components/image-reader/demo/cases/demo0.vue create mode 100644 components/image-reader/demo/cases/demo1.vue create mode 100644 components/image-reader/demo/index.vue create mode 100644 components/image-reader/image-dataurl.js create mode 100644 components/image-reader/image-processor.js create mode 100644 components/image-reader/image-reader.js create mode 100644 components/image-reader/index.vue create mode 100644 components/image-reader/test/file.mock.js create mode 100644 components/image-reader/test/index.spec.js create mode 100644 components/image-viewer/README.md create mode 100644 components/image-viewer/component.js create mode 100644 components/image-viewer/demo/cases/demo0.vue create mode 100644 components/image-viewer/demo/index.vue create mode 100644 components/image-viewer/index.vue create mode 100644 components/image-viewer/test/index.spec.js create mode 100644 components/index.js create mode 100644 components/input-item/README.md create mode 100644 components/input-item/component.js create mode 100644 components/input-item/cursor.js create mode 100644 components/input-item/demo/cases/demo0.vue create mode 100644 components/input-item/demo/cases/demo1.vue create mode 100644 components/input-item/demo/cases/demo2.vue create mode 100644 components/input-item/demo/cases/demo3.vue create mode 100644 components/input-item/demo/cases/demo4.vue create mode 100644 components/input-item/demo/index.vue create mode 100644 components/input-item/index.vue create mode 100644 components/input-item/keycode.js create mode 100644 components/input-item/test/index.spec.js create mode 100644 components/landscape/README.md create mode 100644 components/landscape/component.js create mode 100644 components/landscape/demo/cases/demo0.vue create mode 100644 components/landscape/demo/index.vue create mode 100644 components/landscape/index.vue create mode 100644 components/landscape/test/index.spec.js create mode 100644 components/notice-bar/README.md create mode 100644 components/notice-bar/component.js create mode 100644 components/notice-bar/demo/cases/demo0.vue create mode 100644 components/notice-bar/demo/cases/demo1.vue create mode 100644 components/notice-bar/demo/cases/demo2.vue create mode 100644 components/notice-bar/demo/index.vue create mode 100644 components/notice-bar/index.vue create mode 100644 components/notice-bar/test/index.spec.js create mode 100644 components/number-keyboard/README.md create mode 100644 components/number-keyboard/component.js create mode 100644 components/number-keyboard/demo/cases/demo0.vue create mode 100644 components/number-keyboard/demo/cases/demo1.vue create mode 100644 components/number-keyboard/demo/cases/demo2.vue create mode 100644 components/number-keyboard/demo/index.vue create mode 100644 components/number-keyboard/index.vue create mode 100644 components/number-keyboard/keyboard.vue create mode 100644 components/number-keyboard/test/index.spec.js create mode 100644 components/picker/README.md create mode 100644 components/picker/cascade.js create mode 100644 components/picker/component.js create mode 100644 components/picker/demo/cases/demo0.vue create mode 100644 components/picker/demo/cases/demo1.vue create mode 100644 components/picker/demo/cases/demo2.vue create mode 100644 components/picker/demo/data/district.js create mode 100644 components/picker/demo/data/simple.js create mode 100644 components/picker/demo/index.vue create mode 100644 components/picker/index.vue create mode 100644 components/picker/picker-column.vue create mode 100644 components/picker/test/index.spec.js create mode 100644 components/popup-title-bar/index.vue create mode 100644 components/popup/README.md create mode 100644 components/popup/component.js create mode 100644 components/popup/demo/cases/demo0.vue create mode 100644 components/popup/demo/cases/demo1.vue create mode 100644 components/popup/demo/index.vue create mode 100644 components/popup/index.vue create mode 100644 components/popup/test/index.spec.js create mode 100644 components/popup/test/touch-trigger.js create mode 100644 components/popup/title-bar.vue create mode 100644 components/radio/README.md create mode 100644 components/radio/component.js create mode 100644 components/radio/demo/cases/demo0.vue create mode 100644 components/radio/demo/cases/demo1.vue create mode 100644 components/radio/demo/cases/demo2.vue create mode 100644 components/radio/demo/cases/demo3.vue create mode 100644 components/radio/demo/cases/demo4.vue create mode 100644 components/radio/demo/index.vue create mode 100644 components/radio/index.vue create mode 100644 components/radio/test/index.spec.js create mode 100644 components/result-page/README.md create mode 100644 components/result-page/component.js create mode 100644 components/result-page/demo/cases/demo0.vue create mode 100644 components/result-page/demo/cases/demo1.vue create mode 100644 components/result-page/demo/cases/demo2.vue create mode 100644 components/result-page/demo/cases/demo3.vue create mode 100644 components/result-page/demo/index.vue create mode 100644 components/result-page/index.vue create mode 100644 components/result-page/test/index.spec.js create mode 100644 components/selector/README.md create mode 100644 components/selector/component.js create mode 100644 components/selector/demo/cases/demo0.vue create mode 100644 components/selector/demo/cases/demo1.vue create mode 100644 components/selector/demo/cases/demo2.vue create mode 100644 components/selector/demo/cases/demo3.vue create mode 100644 components/selector/demo/index.vue create mode 100644 components/selector/index.vue create mode 100644 components/selector/test/index.spec.js create mode 100644 components/stepper/README.md create mode 100644 components/stepper/component.js create mode 100644 components/stepper/demo/cases/demo0.vue create mode 100644 components/stepper/demo/cases/demo1.vue create mode 100644 components/stepper/demo/cases/demo2.vue create mode 100644 components/stepper/demo/cases/demo3.vue create mode 100644 components/stepper/demo/cases/demo4.vue create mode 100644 components/stepper/demo/cases/demo5.vue create mode 100644 components/stepper/demo/index.vue create mode 100644 components/stepper/index.vue create mode 100644 components/stepper/test/index.spec.js create mode 100644 components/steps/README.md create mode 100644 components/steps/component.js create mode 100644 components/steps/demo/cases/demo0.vue create mode 100644 components/steps/demo/cases/demo1.vue create mode 100644 components/steps/demo/cases/demo2.vue create mode 100644 components/steps/demo/cases/demo3.vue create mode 100644 components/steps/demo/cases/demo4.vue create mode 100644 components/steps/demo/cases/demo5.vue create mode 100644 components/steps/demo/cases/demo6.vue create mode 100644 components/steps/demo/index.vue create mode 100644 components/steps/index.vue create mode 100644 components/steps/test/index.spec.js create mode 100644 components/swiper-item/index.vue create mode 100644 components/swiper/README.md create mode 100644 components/swiper/component.js create mode 100644 components/swiper/demo/cases/demo0.vue create mode 100644 components/swiper/demo/cases/demo1.vue create mode 100644 components/swiper/demo/cases/demo2.vue create mode 100644 components/swiper/demo/cases/demo3.vue create mode 100644 components/swiper/demo/data/mulit-item.js create mode 100644 components/swiper/demo/data/simple.js create mode 100644 components/swiper/demo/index.vue create mode 100644 components/swiper/index.vue create mode 100644 components/swiper/swiper-item.vue create mode 100644 components/swiper/test/index.spec.js create mode 100644 components/switch/README.md create mode 100644 components/switch/component.js create mode 100644 components/switch/demo/cases/demo0.vue create mode 100644 components/switch/demo/cases/demo1.vue create mode 100644 components/switch/demo/cases/demo2.vue create mode 100644 components/switch/demo/cases/demo3.vue create mode 100644 components/switch/demo/index.vue create mode 100644 components/switch/index.vue create mode 100644 components/switch/test/index.spec.js create mode 100644 components/tab-bar/README.md create mode 100644 components/tab-bar/component.js create mode 100644 components/tab-bar/demo/cases/demo0.vue create mode 100644 components/tab-bar/demo/cases/demo1.vue create mode 100644 components/tab-bar/demo/cases/demo2.vue create mode 100644 components/tab-bar/demo/cases/demo3.vue create mode 100644 components/tab-bar/demo/cases/demo4.vue create mode 100644 components/tab-bar/demo/cases/demo5.vue create mode 100644 components/tab-bar/demo/cases/demo6.vue create mode 100644 components/tab-bar/demo/cases/demo7.vue create mode 100644 components/tab-bar/demo/cases/demo8.vue create mode 100644 components/tab-bar/demo/cases/demo9.vue create mode 100644 components/tab-bar/demo/index.vue create mode 100644 components/tab-bar/index.vue create mode 100644 components/tab-bar/test/index.spec.js create mode 100644 components/tab-picker/README.md create mode 100644 components/tab-picker/component.js create mode 100644 components/tab-picker/demo/cases/demo0.vue create mode 100644 components/tab-picker/demo/cases/demo1.vue create mode 100644 components/tab-picker/demo/cases/demo2.vue create mode 100644 components/tab-picker/demo/data/cascade.js create mode 100644 components/tab-picker/demo/data/no-cascade.js create mode 100644 components/tab-picker/demo/index.vue create mode 100644 components/tab-picker/index.vue create mode 100644 components/tab-picker/test/index.spec.js create mode 100644 components/tabs/README.md create mode 100644 components/tabs/component.js create mode 100644 components/tabs/demo/cases/demo0.vue create mode 100644 components/tabs/demo/cases/demo1.vue create mode 100644 components/tabs/demo/cases/demo10.vue create mode 100644 components/tabs/demo/cases/demo11.vue create mode 100644 components/tabs/demo/cases/demo2.vue create mode 100644 components/tabs/demo/cases/demo3.vue create mode 100644 components/tabs/demo/cases/demo4.vue create mode 100644 components/tabs/demo/cases/demo5.vue create mode 100644 components/tabs/demo/cases/demo6.vue create mode 100644 components/tabs/demo/cases/demo7.vue create mode 100644 components/tabs/demo/cases/demo8.vue create mode 100644 components/tabs/demo/cases/demo9.vue create mode 100644 components/tabs/demo/index.vue create mode 100644 components/tabs/index.vue create mode 100644 components/tabs/test/index.spec.js create mode 100644 components/tag/README.md create mode 100644 components/tag/component.js create mode 100644 components/tag/demo/cases/demo0.vue create mode 100644 components/tag/demo/cases/demo1.vue create mode 100644 components/tag/demo/cases/demo2.vue create mode 100644 components/tag/demo/cases/demo3.vue create mode 100644 components/tag/demo/index.vue create mode 100644 components/tag/index.vue create mode 100644 components/tag/test/index.spec.js create mode 100644 components/tip/README.md create mode 100644 components/tip/component.js create mode 100644 components/tip/demo/cases/demo0.vue create mode 100644 components/tip/demo/cases/demo1.vue create mode 100644 components/tip/demo/cases/demo2.vue create mode 100644 components/tip/demo/cases/demo3.vue create mode 100644 components/tip/demo/cases/demo4.vue create mode 100644 components/tip/demo/index.vue create mode 100644 components/tip/index.js create mode 100644 components/tip/test/index.spec.js create mode 100644 components/tip/tip.vue create mode 100644 components/toast/README.md create mode 100644 components/toast/component.js create mode 100644 components/toast/demo/cases/demo0.vue create mode 100644 components/toast/demo/cases/demo1.vue create mode 100644 components/toast/demo/cases/demo2.vue create mode 100644 components/toast/demo/cases/demo3.vue create mode 100644 components/toast/demo/cases/demo4.vue create mode 100644 components/toast/demo/cases/demo5.vue create mode 100644 components/toast/demo/index.vue create mode 100644 components/toast/index.js create mode 100644 components/toast/test/index.spec.js create mode 100644 components/toast/toast.vue create mode 100644 config/dev.env.js create mode 100644 config/index.js create mode 100644 config/prod.env.js create mode 100644 config/test.env.js create mode 100644 examples/App.vue create mode 100644 examples/assets/images/bank-zs.svg create mode 100644 examples/assets/images/cashier-icon-1.png create mode 100644 examples/assets/images/cashier-icon-2.png create mode 100644 examples/assets/images/cashier-icon-3.png create mode 100644 examples/assets/images/cashier-icon-4.png create mode 100644 examples/assets/images/cashier-icon-5.png create mode 100644 examples/assets/responsive.js create mode 100644 examples/category.vue create mode 100644 examples/components.json create mode 100644 examples/create-demo-module.js create mode 100644 examples/demo-index.indemand.js create mode 100644 examples/demo-index.js create mode 100644 examples/home.indemand.vue create mode 100644 examples/home.vue create mode 100644 examples/index.html create mode 100644 examples/main.indemand.js create mode 100644 examples/main.js create mode 100644 examples/route.indemand.js create mode 100644 examples/route.js create mode 100644 examples/single-component-app.vue create mode 100644 examples/single-component-main.js create mode 100644 examples/theme.custom.styl create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 site/.babelrc create mode 100644 site/.eslintignore create mode 100644 site/.eslintrc.js create mode 100644 site/.gitignore create mode 100644 site/.postcssrc.js create mode 100644 site/build/bin/default.mfe.blog.config.js create mode 100644 site/build/bin/gen-indices.js create mode 100644 site/build/bin/markdown.js create mode 100644 site/build/bin/mfe-blog-dev.js create mode 100644 site/build/bin/mfe-blog-generate.js create mode 100644 site/build/bin/utils.js create mode 100644 site/build/build.js create mode 100644 site/build/check-versions.js create mode 100644 site/build/dev-client.js create mode 100644 site/build/dev-server.js create mode 100644 site/build/utils.js create mode 100644 site/build/vue-loader.conf.js create mode 100644 site/build/webpack.base.conf.js create mode 100644 site/build/webpack.dev.conf.js create mode 100644 site/build/webpack.prod.conf.js create mode 100644 site/config/dev.env.js create mode 100644 site/config/index.js create mode 100644 site/config/prod.env.js create mode 100644 site/index.html create mode 100644 site/mfe.blog.config.js create mode 100644 site/package.json create mode 100644 site/theme/default/App.vue create mode 100644 site/theme/default/Error.vue create mode 100644 site/theme/default/Home.vue create mode 100644 site/theme/default/Preview.vue create mode 100644 site/theme/default/assets/css/demo.styl create mode 100644 site/theme/default/assets/css/global.styl create mode 100644 site/theme/default/assets/css/hightlight.css create mode 100644 site/theme/default/assets/css/markdown.styl create mode 100644 site/theme/default/assets/css/mixin.styl create mode 100644 site/theme/default/assets/css/toc.styl create mode 100644 site/theme/default/assets/js/home.config.js create mode 100644 site/theme/default/assets/js/responsive.js create mode 100644 site/theme/default/assets/js/util.js create mode 100644 site/theme/default/components/Doc.vue create mode 100644 site/theme/default/components/Footer.vue create mode 100644 site/theme/default/components/Header.vue create mode 100644 site/theme/default/components/Menu.vue create mode 100644 site/theme/default/components/Table.vue create mode 100644 site/theme/default/main.js create mode 100644 site/theme/default/router/index.js create mode 100644 static/.gitkeep create mode 100644 static/animate.css create mode 100644 static/jquery.lettering.js create mode 100644 static/jquery.textillate.js create mode 100644 static/pace.css create mode 100644 static/pace.js create mode 100644 test/unit/.eslintrc create mode 100644 test/unit/index.js create mode 100644 test/unit/index.rollup.js create mode 100644 test/unit/karma.conf.js create mode 100644 test/unit/rollup.karma.conf.js create mode 100644 types/component.d.ts create mode 100644 types/dialog.d.ts create mode 100644 types/image-processor.d.ts create mode 100644 types/index.d.ts create mode 100644 types/toast.d.ts diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..b0cf70c5 --- /dev/null +++ b/.babelrc @@ -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"] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9d08a1a8 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..c7388938 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,11 @@ +build/*.js +config/*.js +lib/* +output/* +examples/* +site/* +**/*.spec.* +**/demo/data/** +scroller.js +animate.js +gulpfile.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..cdaea3fe --- /dev/null +++ b/.eslintrc.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'], +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dd22c00d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ffaa5e1d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,80 @@ +--- +title: 更新日志 +--- + + +### 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 + + + +### 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** + - 修复部分文档,样式和错误 + + +### 0.1.0 + +`2017-11-21` + +- **Feature** + + - 完成开发版开发,用于内部体验和测试 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7d4c2cfc --- /dev/null +++ b/CONTRIBUTING.md @@ -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. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cee01710 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..1c7c6e8a --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +
+ + LOGO + +
+
+ + + + + + + + + +
+ +# mand-mobile + +[![](https://img.shields.io/travis/didi/mand-mobile.svg?style=flat-square)](https://travis-ci.org/adidi/mand-mobile) +[![Codecov](https://img.shields.io/codecov/c/github/didi/mand-mobile/master.svg?style=flat-square)](https://codecov.io/gh/didi/mand-mobile/branch/master) +[![npm package](https://img.shields.io/npm/v/mand-mobile.svg?style=flat-square)](https://www.npmjs.org/package/mand-mobile) +[![NPM downloads](http://img.shields.io/npm/dm/mand-mobile.svg?style=flat-square)](http://npmtrends.com/mand-mobile) +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/didi/mand-mobile.svg)](http://isitmaintained.com/project/didi/mand-mobile "Average time to resolve an issue") +[![Percentage of issues still open](http://isitmaintained.com/badge/open/didi/mand-mobile.svg)](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/) + +![Examples Link](./assets/examples-qrcode.png) + +## Install & Usage + +### Install + +```shell +npm install mand-mobile --save +``` + +### Import + +* Use babel-plugin-import + or + ts-import-plugin (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 + + + +``` diff --git a/assets/examples-qrcode.png b/assets/examples-qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..96d69f6eb734f1105903929f0e05dedfc04cfbe4 GIT binary patch literal 643 zcmV-}0(||6P)9n@C7Er-74G*)~lq? zFqft+Ra&}a&>!=^y#vG8(&pYN%@}~lj9fL1-0j@Bi^Ym|7jngAcx6kI!OxAvRA}bZ zwomp@0lFmf$6F;Zb7`?wQy#OpB(3a{!@-#;{P-(0Nq#`y@NydbaVGbbqo6#b4VwK= zfmUVA-HFR!pN;3J%&D@=7&j^nt>;Ey=F)=uqujNzi(w0rlret-Gk`D6jO3RHaDTuG zDKKM4!3KI-m|=cryq?R%x0x&B)+tXf#hCvT4;37t8JSAE*{iZ6cWT--Gh~a)T+#|ONm>u-h97@CLrMzFt%Ey@u$A&r9pZThPHm&;9Sqja}>!i004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv00000008+zyMF)x010qNS#tmY3labT3lag+-G2N401Hw{L_t(|+U=cvY!qc4 z$3HXM@+xgXs2W>*sfUz-`hQ#LZ zg=nKWQ2r2;+LKyH5r_xTq);qZXp5;Pzf{xLa~~ySe96^ zY0`)ZECt>I&ZFM!L^^N9xYUYSlO)B!OUUn9)uuXCyA^w}>Ldn2?H%Y zNM_K~V&yK7EJvT(Q$6)BS8bwGD#*@mTDldpa_M^vlw6s*%p{=d$Qk{Z= z0$X*i)e#2`!!UT@fd@Ev@E{Eh4V0Ics}5<1xL(FHz8H8&b+kcd(V|5hK75!r-+Ys) zQ>Us9=ON;(%n{G{HTdX(zMx_l22VWk1jmmbXVIcXDnkhYe;xTpGtTd8R7RO;TzB1d z)YsSZ+;h)i7|NcJ{0k$z9%=j^0B0R!&6t>`$(Ai!*s)^=p^%c3=}e@zmtz`HV5O?$ zWCLr~tl^`NK4RRsajCzUbB1}FbH&fOzzUU#v5OFbXf&F1{r>)b+x5qfAJ2pd6Vev@ zvSrJ7|NZw_wQ3cyn5QeGt^n2pzm5FI5)=$FBr|~HwQ_TF#cj9UCRVIiAu1~?MN?B# z$^$=DRaGk5vLxOoVyiaZS=?1sRidx2FKP21d+af<U3=}dV*B>( zVsLQKvGKjVy&@bAdl_%@NYjf{9yviaen@xUeYZGu>Xc*ij~zQELZOhCu@=GQtNMj# zQsS$xzT&RC?qbiLJ&x*?m6h@2lTUgX>M*aX&_{e)6bgkzb#=9)qdppqis{p*dlh39 zTt4FGdHIpcWMZ)x>({Sm=gytB>*VF-@$9qDdKKa&uS3XfIrI^qM&;z>i2eKb+dk?q zT(}?#3JSc4?II8|k$bqfwVg-SGB7Z}s#UA_`RAW)m7g+Y3XebjxEG;K1WHT-yq=oh z>Fn&}>8GEzU4HfI)n0`bHjz6uXOXCtk2)1n5 zV!QmZWy`z>>t++VFPp|iYHMq0YHG4o_Wt|t_adYla*&(x1q#D3$j{GrR3{RND2;jN z&Yise`s-tsD=#l+(xgdTym-;gaAtsQ%W=` z(c0QddwaXBva@H;b~~JDCVI}hXB<6x)KDp5PUcM zN~~OIZ(3q+Z?Elvo)l;|fzzs!lIi5+8R*x1Ogzy9iWNT*HUjOxT>IuAem zFvZ2iw#t6~`R87QbjAdZt4vC!lbf5%3opE2yZpx=f9yqA$4#IW=u?!JjA!GdLO zJH$z(*Te3|*Ls$r48t%CRC--jQhOhF))0c zyWfC41Yk+iWOk@XmQX0f)TvXs@x~hohr`@^@4YNoups5N?K!7<-dUrFF%Ia#%r&=#)~h$==IxnAicfBNaF{A zw-x3kqj~0;XV|o9lZv-~8|m#O%mjrzU1T-Wi9{ki_~3(V-MUr9+sBA=Fvm0gEbuQC z5oRKX4jtmoJMVP7{#_c`h4ge7i6&IMp(@HuArgu3)KgEfaN$BaI+R{E{UhEV6B+*> zM%lVblbBzA`Gu;gDrV1~&Gzlv85|r;{Y8_f9_i)(kuyrA=Yc~iqYe^XU0uBM&O2<| zwvF$<|6X-CLp-18AEOz67`aWQ@3t8A3te4Z)YaAT;fEhmS69ctz<}zIuH-YMhjGan zqw?R8dzobAu7VzU}h3xEw)r-8d^C#6U(%51Ha3EbTW&jPSKZR+H zt_JL##qAZcxCQtcy-Jo}=>3uXdpu2kMPz)bjy*p(&dXOg-^`Nkyo+@GV@NP0{Ap^F4%|qYVaiZOLIQH%ZYtg6v zu}Wy(r`>Zx<5-=C|02n~MS*dEkYRg>JNevRkz)Q%jQ&b!1 zRPDfKs|}hYF_ANN-$KDUd=7Npig77fI?2>ca*?YuZUvfAZ+5d4%Mz|wJTM3Zz5iGQ zECdz+^MKodi7vfYA99iZBgmz{8?hDy4kAG}ek38F1bKtwCg5h^24DtoH82f11#J9? wdi`9U9C{A97hgMahpscoeS2ETpw+ { + 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() diff --git a/build/mand-change-log.js b/build/mand-change-log.js new file mode 100644 index 00000000..1d345289 --- /dev/null +++ b/build/mand-change-log.js @@ -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) + }) + } +} \ No newline at end of file diff --git a/build/rollup/build-component.rollup.js b/build/rollup/build-component.rollup.js new file mode 100644 index 00000000..fbffe394 --- /dev/null +++ b/build/rollup/build-component.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/build-example.rollup.js b/build/rollup/build-example.rollup.js new file mode 100644 index 00000000..9b0da805 --- /dev/null +++ b/build/rollup/build-example.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/build-mand-mobile.rollup.js b/build/rollup/build-mand-mobile.rollup.js new file mode 100644 index 00000000..7c6a91b8 --- /dev/null +++ b/build/rollup/build-mand-mobile.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/dev-server.rollup.js b/build/rollup/dev-server.rollup.js new file mode 100644 index 00000000..2e868194 --- /dev/null +++ b/build/rollup/dev-server.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/rollup-plugin-config.js b/build/rollup/rollup-plugin-config.js new file mode 100644 index 00000000..3c863c1f --- /dev/null +++ b/build/rollup/rollup-plugin-config.js @@ -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, +} \ No newline at end of file diff --git a/build/stylus-mixin.js b/build/stylus-mixin.js new file mode 100644 index 00000000..689392de --- /dev/null +++ b/build/stylus-mixin.js @@ -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')) +} \ No newline at end of file diff --git a/build/template.exp b/build/template.exp new file mode 100644 index 00000000..bc1583f2 --- /dev/null +++ b/build/template.exp @@ -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 + diff --git a/build/webpack/build-example.js b/build/webpack/build-example.js new file mode 100644 index 00000000..c417aa61 --- /dev/null +++ b/build/webpack/build-example.js @@ -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' + )) + }) +}) diff --git a/build/webpack/build-mand-mobile.js b/build/webpack/build-mand-mobile.js new file mode 100644 index 00000000..d07963ab --- /dev/null +++ b/build/webpack/build-mand-mobile.js @@ -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')) +}) diff --git a/build/webpack/build-style-entry.js b/build/webpack/build-style-entry.js new file mode 100644 index 00000000..4c980c15 --- /dev/null +++ b/build/webpack/build-style-entry.js @@ -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')) +}) + diff --git a/build/webpack/dev-client.js b/build/webpack/dev-client.js new file mode 100644 index 00000000..2f75dd53 --- /dev/null +++ b/build/webpack/dev-client.js @@ -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() + } +}) diff --git a/build/webpack/dev-server.js b/build/webpack/dev-server.js new file mode 100644 index 00000000..7cf2bc28 --- /dev/null +++ b/build/webpack/dev-server.js @@ -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() + } +} diff --git a/build/webpack/utils.js b/build/webpack/utils.js new file mode 100644 index 00000000..a0700644 --- /dev/null +++ b/build/webpack/utils.js @@ -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 +} diff --git a/build/webpack/vue-loader.conf.js b/build/webpack/vue-loader.conf.js new file mode 100644 index 00000000..3a69c882 --- /dev/null +++ b/build/webpack/vue-loader.conf.js @@ -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' + } +} diff --git a/build/webpack/webpack.base.conf.js b/build/webpack/webpack.base.conf.js new file mode 100644 index 00000000..b565b813 --- /dev/null +++ b/build/webpack/webpack.base.conf.js @@ -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')] + } + ] + } +} diff --git a/build/webpack/webpack.build.conf.js b/build/webpack/webpack.build.conf.js new file mode 100644 index 00000000..67367f3e --- /dev/null +++ b/build/webpack/webpack.build.conf.js @@ -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 \ No newline at end of file diff --git a/build/webpack/webpack.dev.conf.js b/build/webpack/webpack.dev.conf.js new file mode 100644 index 00000000..abf99805 --- /dev/null +++ b/build/webpack/webpack.dev.conf.js @@ -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() + ] +}) diff --git a/build/webpack/webpack.example.conf.js b/build/webpack/webpack.example.conf.js new file mode 100644 index 00000000..336a82a8 --- /dev/null +++ b/build/webpack/webpack.example.conf.js @@ -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 diff --git a/build/webpack/webpack.test.conf.js b/build/webpack/webpack.test.conf.js new file mode 100644 index 00000000..ace7d10e --- /dev/null +++ b/build/webpack/webpack.test.conf.js @@ -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 diff --git a/components/_style/global.styl b/components/_style/global.styl new file mode 100644 index 00000000..8139913a --- /dev/null +++ b/components/_style/global.styl @@ -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 diff --git a/components/_style/images/arrow-down.svg b/components/_style/images/arrow-down.svg new file mode 100644 index 00000000..36c5532c --- /dev/null +++ b/components/_style/images/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/arrow-left.svg b/components/_style/images/arrow-left.svg new file mode 100644 index 00000000..68d344b8 --- /dev/null +++ b/components/_style/images/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/arrow-right.svg b/components/_style/images/arrow-right.svg new file mode 100644 index 00000000..fc969c56 --- /dev/null +++ b/components/_style/images/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/arrow-up.svg b/components/_style/images/arrow-up.svg new file mode 100644 index 00000000..66263a95 --- /dev/null +++ b/components/_style/images/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle-alert.svg b/components/_style/images/circle-alert.svg new file mode 100644 index 00000000..259ffe0d --- /dev/null +++ b/components/_style/images/circle-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle-cross.svg b/components/_style/images/circle-cross.svg new file mode 100644 index 00000000..9ff6aef5 --- /dev/null +++ b/components/_style/images/circle-cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle-right.svg b/components/_style/images/circle-right.svg new file mode 100644 index 00000000..b8408fc9 --- /dev/null +++ b/components/_style/images/circle-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle.svg b/components/_style/images/circle.svg new file mode 100644 index 00000000..7e5632b1 --- /dev/null +++ b/components/_style/images/circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/cross.svg b/components/_style/images/cross.svg new file mode 100644 index 00000000..f6238c5e --- /dev/null +++ b/components/_style/images/cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/hollow-plus.svg b/components/_style/images/hollow-plus.svg new file mode 100644 index 00000000..c85dde5b --- /dev/null +++ b/components/_style/images/hollow-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/keyboard-del-simple.png b/components/_style/images/keyboard-del-simple.png new file mode 100644 index 0000000000000000000000000000000000000000..bb4c74fe46fb10991b7afc4adf01e892b4b2b099 GIT binary patch literal 543 zcmV+)0^t3LP)N~u;a&~6|sp(jWW&=#85LPGBmf!&^4vpXlyM5EF8TdURj$jdMcZ;@}v+)=(F$EZDw<9IX6 zvfqI=@;v{Af;%YuNTGRN9GaT*?i!%|E#kfuoSt zsMqWNQGd61Wo75B+9plYWD5G&st6fT6fG3-((@Yhe&8`==L_c^`bU!?ZBsCeJ)-qJ zUwl+5(Eg)nagJ^_n}4wO2)SZ$^DQB_<-8Aj4|@1{%_8%`Y+HOWn-jFWZ1Zp)@BKx>7Ju-aU)}uU`Ilw? zc>Q>S$Deo_2+dOip)%g{2(GUg!IkmnUpVxf(TqTac3LK^rIPZH47A&Zs6cZc%7C=lHb(^YYJ~HT-PMlSdnZqq+Zic>BjAj2vuC#1{(|IXJ!fKogkQ z=SAoLx;9>W;B!_!w(}6{j>9=r#DHfcB0s+E~rNbZdDz+CW8J z&m^V@BAUwRDA$C!5LaYO(-~?WQ|ge=iIikaGSYEnvAfo`a&9bT@#>iEIukj=!9FOa~NCWf1p`p(#LRfOpq{DMDPi zB~ehg&C1TsE}GBEST}6rwYHfOo{mRJH9S+G8pmXS@^@%HGPtPdWV-_NmG-&3JWPs$ zxIsiU1Q>UmY6A3Xlr;I9=v?aOvB_^U0lGH|KySHXy3dNXYX!xDHuL%29=8w;3V7eZ z4z2Z2Q780iGr!Hi-6XMx!-ozxr-CYF2t*M&%MP+jN!UXH#0)r|HT&d}x&vaG#>8E)P9 zNT^uus8&!1b-yW4k$o;bb~`NdEW$S8LZQ(AE}*BigNg&f0tXCL?01AaCUjD51tv4g zV`WqcvkT&fUQh`Y2X;_5s535}Bp-`Y0Dqd-A~h~TRe64o@2~6Wl-x7*kfHz5Gl*2g z$pUQWBi28G*9&<4Y5GP7Ia3dd6>KN~l~9jZG!>u{s=9%(1(h>3*5auFl`}QY0xG=6 zUd%?sti&QJ0+mn`EM#tcWb8~$vY@k)Rd~vonjGZjPDGtmdCHlZ z5=2m7Zk8yNqoxK7G!9YxKd&C*?hfoAEdY==5QEjIF;IzUG-5&_j3H=+Z8R|2$8YWQ ViVWFInL+>n002ovPDHLkV1j*3vswTE literal 0 HcmV?d00001 diff --git a/components/_style/images/keyboard-hide.png b/components/_style/images/keyboard-hide.png new file mode 100644 index 0000000000000000000000000000000000000000..339ccd6edcd51256cc8e3a7b996adc75cb5d5aaa GIT binary patch literal 493 zcmeAS@N?(olHy`uVBq!ia0vp^ML-@V9tlL@)1X$Q5?>zh${`b=BdDGpiaZFUzu(+AESgKSXdhO09AUvN3a7(5NNK z>g0G(TA%B{ zLyr%9?%z8-@Y^5e+aJ5mhkr@-%`9epb>J=IJ;v{fYo)ihZ)Cs2{;q$gZNzi!pX}4( zYu3bVn)*lcLAp=L=jEU0Z_GFtKkK2s!^i12=WU&FBF@N^=db~fGQ;5&={?OYsh7Cb vI8X0+A$eLSMZG5k9bM&6zs|9I{YUW$l@Yto7Fs`b literal 0 HcmV?d00001 diff --git a/components/_style/images/right.svg b/components/_style/images/right.svg new file mode 100644 index 00000000..d9903b20 --- /dev/null +++ b/components/_style/images/right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/spinner.svg b/components/_style/images/spinner.svg new file mode 100644 index 00000000..1e8e05fc --- /dev/null +++ b/components/_style/images/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/mixin/theme.styl b/components/_style/mixin/theme.styl new file mode 100644 index 00000000..03c19d98 --- /dev/null +++ b/components/_style/mixin/theme.styl @@ -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 diff --git a/components/_style/mixin/util.styl b/components/_style/mixin/util.styl new file mode 100644 index 00000000..67f38861 --- /dev/null +++ b/components/_style/mixin/util.styl @@ -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 \ No newline at end of file diff --git a/components/_util/animate.js b/components/_util/animate.js new file mode 100644 index 00000000..c226390b --- /dev/null +++ b/components/_util/animate.js @@ -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 diff --git a/components/_util/debug.js b/components/_util/debug.js new file mode 100644 index 00000000..1fcb64a5 --- /dev/null +++ b/components/_util/debug.js @@ -0,0 +1,5 @@ +import {isProd} from './env' + +export const warn = (msg, fn = 'error') => { + !isProd && console[fn](`[Mand-Mobile]: ${msg}`) +} diff --git a/components/_util/env.js b/components/_util/env.js new file mode 100644 index 00000000..d9b90f74 --- /dev/null +++ b/components/_util/env.js @@ -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) diff --git a/components/_util/formate-value.js b/components/_util/formate-value.js new file mode 100644 index 00000000..1491592c --- /dev/null +++ b/components/_util/formate-value.js @@ -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 +} diff --git a/components/_util/index.js b/components/_util/index.js new file mode 100644 index 00000000..b08a0d2c --- /dev/null +++ b/components/_util/index.js @@ -0,0 +1,4 @@ +export * from './debug' +export * from './env' +export * from './store' +export * from './lang' diff --git a/components/_util/lang.js b/components/_util/lang.js new file mode 100644 index 00000000..a1de47f7 --- /dev/null +++ b/components/_util/lang.js @@ -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)}` +} diff --git a/components/_util/render.js b/components/_util/render.js new file mode 100644 index 00000000..83353ae7 --- /dev/null +++ b/components/_util/render.js @@ -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) diff --git a/components/_util/scroller.js b/components/_util/scroller.js new file mode 100644 index 00000000..684a9b42 --- /dev/null +++ b/components/_util/scroller.js @@ -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 null + * @param top {Number?null} Vertical scroll position, keeps current if value is null + * @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) diff --git a/components/_util/store.js b/components/_util/store.js new file mode 100644 index 00000000..af28007d --- /dev/null +++ b/components/_util/store.js @@ -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 +} diff --git a/components/action-bar/README.md b/components/action-bar/README.md new file mode 100644 index 00000000..a4b1bc74 --- /dev/null +++ b/components/action-bar/README.md @@ -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) +``` + +### 代码演示 + + + +### API + +#### ActionBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|actions|按钮组|Array<{text, disabled, onClick}>|-|`text`为按钮文案,
`disabled`为是否禁用改按钮,
`onClick`为点击事件响应函数,传参数与`click`事件相同| +|has-text|是否显示文案|Boolean|是否含有`slot`|文案可通过`slot`传入| + + +#### ActionBar Events + +##### @click(event, action) +按钮点击事件 + +|属性 | 说明 | 类型 | +|----|-----|------| +|action|actions列表中与被点击按钮对应的对象|Object: {text, disabled, ...}| diff --git a/components/action-bar/component.js b/components/action-bar/component.js new file mode 100644 index 00000000..1031779e --- /dev/null +++ b/components/action-bar/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'action-bar', + 'text': '操作栏', + 'category': 'basic', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/action-bar/demo/cases/demo0.vue b/components/action-bar/demo/cases/demo0.vue new file mode 100644 index 00000000..f41ed2d9 --- /dev/null +++ b/components/action-bar/demo/cases/demo0.vue @@ -0,0 +1,36 @@ + + + diff --git a/components/action-bar/demo/cases/demo1.vue b/components/action-bar/demo/cases/demo1.vue new file mode 100644 index 00000000..864406e6 --- /dev/null +++ b/components/action-bar/demo/cases/demo1.vue @@ -0,0 +1,40 @@ + + + diff --git a/components/action-bar/demo/cases/demo2.vue b/components/action-bar/demo/cases/demo2.vue new file mode 100644 index 00000000..38fb45ab --- /dev/null +++ b/components/action-bar/demo/cases/demo2.vue @@ -0,0 +1,35 @@ + + + diff --git a/components/action-bar/demo/index.vue b/components/action-bar/demo/index.vue new file mode 100644 index 00000000..7a9d382d --- /dev/null +++ b/components/action-bar/demo/index.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/components/action-bar/index.vue b/components/action-bar/index.vue new file mode 100644 index 00000000..00acb600 --- /dev/null +++ b/components/action-bar/index.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/components/action-bar/test/index.spec.js b/components/action-bar/test/index.spec.js new file mode 100644 index 00000000..f4e9eb32 --- /dev/null +++ b/components/action-bar/test/index.spec.js @@ -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) + }) +}) diff --git a/components/action-sheet/README.md b/components/action-sheet/README.md new file mode 100644 index 00000000..002c97b2 --- /dev/null +++ b/components/action-sheet/README.md @@ -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) +``` + +### 代码演示 + + +### 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() +面板隐藏事件 diff --git a/components/action-sheet/component.js b/components/action-sheet/component.js new file mode 100644 index 00000000..216a7cb6 --- /dev/null +++ b/components/action-sheet/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'action-sheet', + 'text': '底部弹窗', + 'category': 'feedback', + 'description': '', + 'author': 'qiman' +} diff --git a/components/action-sheet/demo/cases/demo0.vue b/components/action-sheet/demo/cases/demo0.vue new file mode 100644 index 00000000..2b7e7f97 --- /dev/null +++ b/components/action-sheet/demo/cases/demo0.vue @@ -0,0 +1,67 @@ + + + diff --git a/components/action-sheet/demo/index.vue b/components/action-sheet/demo/index.vue new file mode 100644 index 00000000..682e177a --- /dev/null +++ b/components/action-sheet/demo/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/components/action-sheet/index.vue b/components/action-sheet/index.vue new file mode 100644 index 00000000..504aacf7 --- /dev/null +++ b/components/action-sheet/index.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/components/action-sheet/test/index.spec.js b/components/action-sheet/test/index.spec.js new file mode 100644 index 00000000..99e7e33b --- /dev/null +++ b/components/action-sheet/test/index.spec.js @@ -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() + }) + }) +}) diff --git a/components/agree/README.md b/components/agree/README.md new file mode 100644 index 00000000..f6bf2d2d --- /dev/null +++ b/components/agree/README.md @@ -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) +``` + +### 代码演示 + + +### API + +#### Agree Props +|属性 | 说明 | 类型 | 默认值 | +|----|-----|------|------| +|v-model|是否选中|Boolean|`false`| +|disabled|是否禁用|Boolean|`false`| +|size|按钮大小,可选值同icon|String|`md`| + +#### Agree Events + +##### @change(name, checked) +勾选状态发生变化事件 + +|属性 | 说明 | 类型 | +|----|-----|------| +|name|单选按钮名称,唯一标识|Number/String| +|checked|是否选中|Boolean| diff --git a/components/agree/component.js b/components/agree/component.js new file mode 100644 index 00000000..bf904191 --- /dev/null +++ b/components/agree/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'agree', + 'text': '单选框', + 'category': 'form', + 'description': '', + 'author': 'chengyanjing' +} diff --git a/components/agree/demo/cases/demo0.vue b/components/agree/demo/cases/demo0.vue new file mode 100644 index 00000000..3e75b9bd --- /dev/null +++ b/components/agree/demo/cases/demo0.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/components/agree/demo/cases/demo1.vue b/components/agree/demo/cases/demo1.vue new file mode 100644 index 00000000..f5db5198 --- /dev/null +++ b/components/agree/demo/cases/demo1.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/components/agree/demo/cases/demo2.vue b/components/agree/demo/cases/demo2.vue new file mode 100644 index 00000000..2fd675b0 --- /dev/null +++ b/components/agree/demo/cases/demo2.vue @@ -0,0 +1,40 @@ + + + diff --git a/components/agree/demo/cases/demo3.vue b/components/agree/demo/cases/demo3.vue new file mode 100644 index 00000000..3348f200 --- /dev/null +++ b/components/agree/demo/cases/demo3.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/components/agree/demo/index.vue b/components/agree/demo/index.vue new file mode 100644 index 00000000..61b17994 --- /dev/null +++ b/components/agree/demo/index.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/components/agree/index.vue b/components/agree/index.vue new file mode 100644 index 00000000..921da787 --- /dev/null +++ b/components/agree/index.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/components/agree/test/index.spec.js b/components/agree/test/index.spec.js new file mode 100644 index 00000000..89d5ca1a --- /dev/null +++ b/components/agree/test/index.spec.js @@ -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 + }) +}) diff --git a/components/button/README.md b/components/button/README.md new file mode 100644 index 00000000..2af5f92c --- /dev/null +++ b/components/button/README.md @@ -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) +``` + +### 代码演示 + + +### 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`|-| diff --git a/components/button/demo/cases/demo0.vue b/components/button/demo/cases/demo0.vue new file mode 100644 index 00000000..e458fb02 --- /dev/null +++ b/components/button/demo/cases/demo0.vue @@ -0,0 +1,18 @@ + + + + diff --git a/components/button/demo/cases/demo1.vue b/components/button/demo/cases/demo1.vue new file mode 100644 index 00000000..cff03aa2 --- /dev/null +++ b/components/button/demo/cases/demo1.vue @@ -0,0 +1,20 @@ + + + + diff --git a/components/button/demo/cases/demo2.vue b/components/button/demo/cases/demo2.vue new file mode 100644 index 00000000..90b5604c --- /dev/null +++ b/components/button/demo/cases/demo2.vue @@ -0,0 +1,20 @@ + + + + diff --git a/components/button/demo/cases/demo3.vue b/components/button/demo/cases/demo3.vue new file mode 100644 index 00000000..0e12c83d --- /dev/null +++ b/components/button/demo/cases/demo3.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/components/button/demo/index.vue b/components/button/demo/index.vue new file mode 100644 index 00000000..65aa4a87 --- /dev/null +++ b/components/button/demo/index.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/components/button/index.vue b/components/button/index.vue new file mode 100644 index 00000000..e43ca473 --- /dev/null +++ b/components/button/index.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/components/button/test/index.spec.js b/components/button/test/index.spec.js new file mode 100644 index 00000000..9f470048 --- /dev/null +++ b/components/button/test/index.spec.js @@ -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) + }) +}) diff --git a/components/captcha/README.md b/components/captcha/README.md new file mode 100644 index 00000000..a07930b2 --- /dev/null +++ b/components/captcha/README.md @@ -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) +``` + +### 代码演示 + + +### 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) +用户提交输入内容事件 diff --git a/components/captcha/component.js b/components/captcha/component.js new file mode 100644 index 00000000..d7e4041b --- /dev/null +++ b/components/captcha/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'captcha', + 'text': '验证码窗口', + 'category': 'business', + 'description': '交互式验证码校验弹窗', + 'author': 'liuxinyumichael' +} diff --git a/components/captcha/demo/cases/demo0.vue b/components/captcha/demo/cases/demo0.vue new file mode 100644 index 00000000..f1ca1799 --- /dev/null +++ b/components/captcha/demo/cases/demo0.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/components/captcha/demo/cases/demo1.vue b/components/captcha/demo/cases/demo1.vue new file mode 100644 index 00000000..384b8c86 --- /dev/null +++ b/components/captcha/demo/cases/demo1.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/components/captcha/demo/index.vue b/components/captcha/demo/index.vue new file mode 100644 index 00000000..5d518d43 --- /dev/null +++ b/components/captcha/demo/index.vue @@ -0,0 +1,48 @@ +< + + + + diff --git a/components/captcha/index.vue b/components/captcha/index.vue new file mode 100644 index 00000000..3fb06c07 --- /dev/null +++ b/components/captcha/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/components/captcha/test/index.spec.js b/components/captcha/test/index.spec.js new file mode 100644 index 00000000..68ad6772 --- /dev/null +++ b/components/captcha/test/index.spec.js @@ -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) + }) +}) diff --git a/components/cashier/README.md b/components/cashier/README.md new file mode 100644 index 00000000..b3aa2384 --- /dev/null +++ b/components/cashier/README.md @@ -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) +``` + +### 代码演示 + + +### 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() +收银台弹窗隐藏事件 diff --git a/components/cashier/component.js b/components/cashier/component.js new file mode 100644 index 00000000..1949acd2 --- /dev/null +++ b/components/cashier/component.js @@ -0,0 +1,7 @@ +export default { + "name": "cashier", + "text": "收银台", + "category": "business", + "description": "", + "author": "xuxiaoyan" +} diff --git a/components/cashier/demo/cases/demo0.vue b/components/cashier/demo/cases/demo0.vue new file mode 100644 index 00000000..a0ab2e8e --- /dev/null +++ b/components/cashier/demo/cases/demo0.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/components/cashier/demo/index.vue b/components/cashier/demo/index.vue new file mode 100644 index 00000000..c7e3d818 --- /dev/null +++ b/components/cashier/demo/index.vue @@ -0,0 +1,22 @@ +< + + + + diff --git a/components/cashier/index.vue b/components/cashier/index.vue new file mode 100644 index 00000000..21f299ac --- /dev/null +++ b/components/cashier/index.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/components/cashier/rolling.vue b/components/cashier/rolling.vue new file mode 100644 index 00000000..a3c7f6fe --- /dev/null +++ b/components/cashier/rolling.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/components/cashier/test/index.spec.js b/components/cashier/test/index.spec.js new file mode 100644 index 00000000..77a17d32 --- /dev/null +++ b/components/cashier/test/index.spec.js @@ -0,0 +1,156 @@ +import Cashier from '../index' +import {mount} from 'avoriaz' +import {setTimeout} from 'timers' + +describe('Cashier', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + const channels = [ + { + icon: 'cashier-icon-1', + text: '招商银行储蓄卡(0056)支付', + value: '001', + }, + { + icon: 'cashier-icon-2', + text: '支付宝支付', + value: '002', + }, + { + icon: 'cashier-icon-3', + text: '微信支付', + value: '003', + }, + { + icon: 'cashier-icon-4', + text: 'QQ钱包支付', + value: '004', + }, + { + icon: 'cashier-icon-5', + text: '一网通支付', + value: '005', + }, + ] + + it('cashier default-index', done => { + wrapper = mount(Cashier) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.channels = channels + wrapper.vm.defaultIndex = 1 + wrapper.vm.value = true + wrapper.vm.$nextTick(() => { + expect( + wrapper + .find('.choose-channel-item')[0] + .text() + .trim(), + ).to.equal(channels[1].text) + expect(wrapper.vm.activeChannelIndex).to.equal(1) + + const moreBtn = wrapper.find('.choose-channel-more')[0] + moreBtn.trigger('click') + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.choose-channel-item').length).to.equal(channels.length) + + const item = wrapper.find('.choose-channel-item')[2] + item.trigger('click') + expect(eventStub.calledWith('select')).to.be.true + expect(wrapper.vm.activeChannelIndex).to.equal(2) + + const confirm = wrapper.find('.md-cashier-pay-button')[0] + confirm.trigger('click') + expect(eventStub.calledWith('pay')).to.be.true + + wrapper.vm.value = false + done() + }) + }) + }) + + it('cashier captcha', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('captcha', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-captcha').length > 0).to.be.true + const cancel = wrapper.find('.md-popup-cancel')[0] + cancel.trigger('click') + done() + }) + }) + + it('cashier loading', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('loading', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-loading').length > 0).to.be.true + expect( + wrapper + .find('.md-cashier-block-text')[0] + .text() + .trim(), + ).to.equal('123') + done() + }) + }) + + it('cashier success', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('success', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-success').length > 0).to.be.true + expect( + wrapper + .find('.md-cashier-block-text')[0] + .text() + .trim(), + ).to.equal('123') + done() + }) + }) + + it('cashier fail', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('fail', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-fail').length > 0).to.be.true + expect( + wrapper + .find('.md-cashier-block-text')[0] + .text() + .trim(), + ).to.equal('123') + done() + }) + }) +}) diff --git a/components/chart/README.md b/components/chart/README.md new file mode 100644 index 00000000..6e6d8cea --- /dev/null +++ b/components/chart/README.md @@ -0,0 +1,69 @@ +--- +title: Chart 折线图表 +preview: https://didi.github.io/mand-mobile/examples/chart +--- + +SVG折线图表, 可绘制多条折线并配置不同的显示规则。 + +### 引入 + +```javascript +import { Chart } from 'mand-mobile' + +Vue.component(Chart.name, Chart) +``` + +### 代码演示 + + +### API + +#### Chart Props +| 属性 | 说明 |类型 | 默认值 | 必填 | +|----|-----|------|------|------| +| size | 图表绘制区域大小, 元素可为带单位字符串或者纯数字(默认为px) | Array | `[480, 320]` | 可选| +| max | 纵坐标最大值 | number | 若不填则会自动计算数据中最大值 | 可选| +| min | 纵坐标最表最小值, 建议设置为`0` | number | 若为空则会自动计算数据中最小值 | 可选| +| lines | 纵坐标最多画几条线 | number | `5` | 可选| +| step | 纵坐标递减的单位值 | number | 若为空则根据`lines`, `max`, `min`自动计算平均值 | 可选| +| format | 纵坐标标签格式化函数 | Function | `val => val` | 可选| +| labels | 横坐标的标签 | Array | - | 必填| +| datasets | 数据值, 格式参考下面的说明 | Array | - | 必填| +| shift | 纵坐标偏移量 | Number | 0.6 | 可选| + +#### `datasets` +其为对象数组,每个对象定义了一组折线相关属性, 如下说明 + +```javascript +{ + color: '#ff5858', // 颜色, 可选, 默认为橘色 + theme: 'heat', // 主题, 可选heat, region, 默认为空 + width: 1, // 宽度, 可选, 默认为1 + values: [15, 20] // 数据数组 +} +``` + +#### 覆盖样式 +默认图表样式如下 + +```stylus +.md-chart + line + stroke #ccc + stroke-width 0.5 + stroke-linecap square + path + stroke #fa8919 + stroke-width 1 + stroke-linecap butt + .md-chart-axis-y + text + fill #666 + font-size 0.2rem + text-anchor end + .md-chart-axis-x + text + fill #666 + font-size 0.22rem + text-anchor middle +``` diff --git a/components/chart/component.js b/components/chart/component.js new file mode 100644 index 00000000..3183c9af --- /dev/null +++ b/components/chart/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'chart', + 'text': '折线图表', + 'category': 'business', + 'description': '基于 SVG 的折线图表组件', + 'author': 'liuxinyumichael' +} diff --git a/components/chart/demo/cases/demo0.vue b/components/chart/demo/cases/demo0.vue new file mode 100644 index 00000000..7eb58c2f --- /dev/null +++ b/components/chart/demo/cases/demo0.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/components/chart/demo/cases/demo1.vue b/components/chart/demo/cases/demo1.vue new file mode 100644 index 00000000..7312b4c7 --- /dev/null +++ b/components/chart/demo/cases/demo1.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/components/chart/demo/cases/demo2.vue b/components/chart/demo/cases/demo2.vue new file mode 100644 index 00000000..17e3406c --- /dev/null +++ b/components/chart/demo/cases/demo2.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/components/chart/demo/index.vue b/components/chart/demo/index.vue new file mode 100644 index 00000000..8d4cbda4 --- /dev/null +++ b/components/chart/demo/index.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/components/chart/index.vue b/components/chart/index.vue new file mode 100644 index 00000000..ea364e74 --- /dev/null +++ b/components/chart/index.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/components/chart/test/index.spec.js b/components/chart/test/index.spec.js new file mode 100644 index 00000000..aaac8cc1 --- /dev/null +++ b/components/chart/test/index.spec.js @@ -0,0 +1,92 @@ +import Chart from '../index' +import {mount} from 'avoriaz' + +describe('Chart', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple chart', () => { + wrapper = mount(Chart, { + propsData: { + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + values: [12, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + ], + }, + }) + expect(wrapper.contains('.md-chart-graph')).to.be.true + }) + + it('create a chart with multiple datasets', () => { + wrapper = mount(Chart, { + propsData: { + size: ['7rem', '4rem'], + max: 60, + min: 0, + step: 10, + lines: 5, + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + color: '#5e64ff', + width: 1, + values: [8, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + { + width: 1, + values: [10, 20, 25, 30, 28, 35, 38, 42, 40, 40, 45, 42, 45], + }, + ], + }, + }) + expect(wrapper.vm.paths.length).to.equal(2) + }) + + it('create a heat chart', () => { + wrapper = mount(Chart, { + propsData: { + size: ['7rem', '4rem'], + max: 60, + min: 0, + step: 10, + lines: 5, + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + width: 1, + color: 'red', + theme: 'heat', + values: [8, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + ], + }, + }) + expect(wrapper.contains('#path-fill-gradient-red')).to.be.true + }) + + it('create a region chart', () => { + wrapper = mount(Chart, { + propsData: { + size: ['7rem', '4rem'], + max: 60, + min: 0, + step: 10, + lines: 5, + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + width: 1, + theme: 'region', + values: [8, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + ], + }, + }) + expect(wrapper.contains('.md-chart-path-area')).to.be.true + }) +}) diff --git a/components/codebox/README.md b/components/codebox/README.md new file mode 100644 index 00000000..f08d543d --- /dev/null +++ b/components/codebox/README.md @@ -0,0 +1,44 @@ +--- +title: CodeBox 验证码输入框 +preview: https://didi.github.io/mand-mobile/examples/codebox +--- + +验证码输入框 + +### 引入 + +```javascript +import { Codebox } from 'mand-mobile' + +Vue.component(Codebox.name, Codebox) +``` + +### 代码演示 + + +### API + +#### Codebox Props +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|v-model|验证码字符串|String|-| +|maxlength|字符最大输入长度, 若为`-1`则不限制输入长度|Number|4| +|autofocus|是否直通聚焦拉起键盘, 对系统键盘不生效|Boolean|`false`| +|mask|是否掩码|Boolean|`false`| +|closable|点击输入框及键盘其他区域是否收起键盘|Boolean|`true`| +|ok-text|键盘确认键文案|String|`确认`| +|disorder|数字键盘是否乱序|Boolean|`false`| +|system|是否使用系统默认键盘|Boolean|`false`| + +#### Codebox Methods + +##### focus() +聚焦输入 + +##### blur() +失焦隐藏键盘 + +#### Codebox Events + +#### @submit(code) +用户提交输入内容事件 diff --git a/components/codebox/component.js b/components/codebox/component.js new file mode 100644 index 00000000..9b7f9b22 --- /dev/null +++ b/components/codebox/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'codebox', + 'text': '验证码输入框', + 'category': 'business', + 'description': '验证码/密码输入框组件', + 'author': 'liuxinyumichael' +} diff --git a/components/codebox/demo/cases/demo0.vue b/components/codebox/demo/cases/demo0.vue new file mode 100644 index 00000000..887bb8ad --- /dev/null +++ b/components/codebox/demo/cases/demo0.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/codebox/demo/cases/demo1.vue b/components/codebox/demo/cases/demo1.vue new file mode 100644 index 00000000..eae437dc --- /dev/null +++ b/components/codebox/demo/cases/demo1.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/codebox/demo/cases/demo2.vue b/components/codebox/demo/cases/demo2.vue new file mode 100644 index 00000000..8d00ca7d --- /dev/null +++ b/components/codebox/demo/cases/demo2.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/codebox/demo/cases/demo3.vue b/components/codebox/demo/cases/demo3.vue new file mode 100644 index 00000000..b109e495 --- /dev/null +++ b/components/codebox/demo/cases/demo3.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/codebox/demo/index.vue b/components/codebox/demo/index.vue new file mode 100644 index 00000000..7aa2aff9 --- /dev/null +++ b/components/codebox/demo/index.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/components/codebox/index.vue b/components/codebox/index.vue new file mode 100644 index 00000000..bce3a751 --- /dev/null +++ b/components/codebox/index.vue @@ -0,0 +1,286 @@ + + + + + + diff --git a/components/codebox/test/index.spec.js b/components/codebox/test/index.spec.js new file mode 100644 index 00000000..65dd30bb --- /dev/null +++ b/components/codebox/test/index.spec.js @@ -0,0 +1,113 @@ +import Codebox from '../index' +import sinon from 'sinon' +import {mount} from 'avoriaz' + +describe('Codebox', () => { + let wrapper + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple codebox', () => { + wrapper = mount(Codebox) + + expect(wrapper.hasClass('md-codebox-wrapper')).to.be.true + }) + + it('create a codebox with input', () => { + wrapper = mount(Codebox, { + propsData: { + maxlength: -1, + }, + }) + expect(wrapper.contains('.md-codebox-holder')).to.be.true + }) + + it('create a codebox with custom keyboard', () => { + wrapper = mount(Codebox, { + propsData: { + system: false, + isView: true, + }, + }) + + expect(wrapper.contains('.md-number-keyboard')).to.be.true + }) + + it('emit input/submit events', done => { + wrapper = mount(Codebox, { + propsData: { + isView: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.setData({ + code: '123', + }) + wrapper.first('.keyboard-number-item').trigger('click') + setTimeout(() => { + expect(eventStub.calledWith('input')).to.be.true + expect(eventStub.calledWith('submit', '1231')).to.be.true + done() + }, 0) + }) + + it('click codebox to focus with custom keyboard', done => { + wrapper = mount(Codebox, { + propsData: { + maxlength: -1, + }, + }) + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.first('.md-codebox').trigger('click') + expect(wrapper.vm.focused).to.be.true + wrapper.first('.keyboard-number-item').trigger('click') + wrapper.first('.confirm').trigger('click') + setTimeout(() => { + expect(wrapper.vm.focused).to.be.false + expect(eventStub.calledWith('submit', '1')).to.be.true + done() + }, 0) + }) + + it('click codebox to focus with system keyboard', () => { + wrapper = mount(Codebox, { + propsData: { + system: true, + }, + }) + + wrapper.first('.md-codebox').trigger('click') + expect(wrapper.vm.focused).to.be.true + }) + + it('delete code char after clicking delete button', () => { + wrapper = mount(Codebox, { + data: { + code: '1234', + }, + }) + + wrapper.first('.delete').trigger('click') + expect(wrapper.vm.code).to.equal('123') + }) + + it('enter code char after clicking number button', done => { + wrapper = mount(Codebox, { + propsData: { + maxlength: 4, + isView: true, + }, + data: { + code: '12', + }, + }) + + wrapper.first('.keyboard-number-item').trigger('click') + setTimeout(() => { + expect(wrapper.vm.code).to.equal('121') + done() + }, 0) + }) +}) diff --git a/components/date-picker/README.md b/components/date-picker/README.md new file mode 100644 index 00000000..aed596a4 --- /dev/null +++ b/components/date-picker/README.md @@ -0,0 +1,173 @@ +--- +title: DatePicker 时间选择器 +preview: https://didi.github.io/mand-mobile/examples/date-picker +--- + +选择日期或者时间,支持年/月/日/时/分和按照范围选择 + +### 引入 + +```javascript +import { DatePicker } from 'mand-mobile' + +Vue.component(DatePicker.name, DatePicker) +``` + +### 代码演示 + + +### API + +#### DatePicker Props +|属性 | 说明 | 类型 | 默认值 | 备注 | +|----|-----|------|------|------| +|type|日期选择类型|String|`date`|`date`, `time`, `datetime`, `custom`| +|custom-types|自定义类型包含的`日期元素`, `[yyyy, MM, dd, hh, mm]`|Array|-|仅用于type为`custom`| +|minDate|最小可选日期|Date|-|-| +|maxDate|最大可选日期|Date|-|-| +|default-date|初始选中日期|Date|-|-| +|minute-step|分钟数递增步长|Number|`1`|-| +|unit-text|元素单位展示文案设置|Array|`['年', '月', '日', '时', '分']`|复杂逻辑使用`text-render`| +|text-render|自定义选项展示文案方法|Function(typeFormat, column0Value, column1Value, ...): String|-|如果使用`text-render`则`unit-text`无效, 示例见附录| +|today-text|今天展示文案设置|String|`今天`|使用`&`可占位日期数字,如`&(今天)`| +|half-day-text|上下午展示文案设置|Array|`['上午', '下午']`|-| +|is-twelve-hours|是否为12时制|Boolean|`false`|-| +|is-view|是否内嵌在页面内展示, 否则以弹层形式|Boolean|`false`|-| +|title|选择器标题|String|-|-| +|ok-text|选择器确认文案|String|`确认`|-| +|cancel-text|选择器取消文案|String|`取消`|-| + +#### DatePicker Methods + +##### getFormatDate(format): dateStr +获取特定格式的日期时间字符串(`format`中的`日期元素`需在列数据中存在),需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型 | 默认 | +|----|-----|------|------| +|format|格式|String|`yyyy-MM-dd hh:mm`| + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|dateStr|日期时间字符串|String| + +> 列表项值属性介绍见附录 + +##### getColumnValue(index): activeItemValue +获取某列当前选中项的值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型 | +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|activeItemValue|选中项的值|Object: {text, value, typeFormat}| + +##### getColumnValues(): columnsValue +获取所有列选中项的值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{text, value, typeFormat}>| + +##### getColumnIndex(index): activeItemIndex +获取某列当前选中项的索引值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型 | +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|activeItemIndex|选中项的索引值|Number| + +##### getColumnIndexs(): columnsIndex +获取所有列选中项的索引值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|columnsIndex|所有列选中项的索引值|Array| + +#### DatePicker Events + +##### @initialed() +选择器数据初始化完成事件 + +##### @change(columnIndex, itemIndex, value) +选择器选中项更改事件 + +|参数 | 说明 | 类型 | +|----|-----|------| +|columnIndex|更改列的索引值|Number| +|itemIndex|更改列选中项的索引值|Number| +|value|更改列选中项的的值|Object: {text, value, typeFormat}| + +##### @confirm(columnsValue) +选择器确认选择事件(仅`is-view`为`false`) + +|参数 | 说明 | 类型 | +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{text, value, typeFormat}>| + +#### 附录 + +* columnData + +```javascript + +const columnData = [ + // 年 + [ + { + text: '2017年', // 日期元素展示文案 + value: 2017, // 日期元素数字 + typeFormat: 'yyyy' // 日期元素类型 yyyy, MM, dd, hh, mm, HalfDay + } + ], + // 月, 日,时, 分 + [ + //.., + ], + // 上午/下午 + [ + { + text: '上午', + value: 0, + typeFormat: 'HalfDay' + }, { + text: '下午', + value: 1, + typeFormat: 'HalfDay' + } + ] +] +``` + +* textRender + +```javascript + + export default { + // ... + methods: { + textRender() { + const args = Array.prototype.slice.call(arguments) + const typeFormat = args[0] // 类型 + const column0Value = args[1] // 第1列选中值 + const column1Value = args[2] // 第2列选中值 + const column2Value = args[3] // 第2列选中值 + }, + } + // ... + } +``` diff --git a/components/date-picker/component.js b/components/date-picker/component.js new file mode 100644 index 00000000..97b650cf --- /dev/null +++ b/components/date-picker/component.js @@ -0,0 +1,7 @@ +export default { + "name": "date-picker", + "text": "日期选择器", + "category": "feedback", + "description": "", + "author": "xuxiaoyan" +} diff --git a/components/date-picker/demo/cases/demo0.vue b/components/date-picker/demo/cases/demo0.vue new file mode 100644 index 00000000..59caeecd --- /dev/null +++ b/components/date-picker/demo/cases/demo0.vue @@ -0,0 +1,37 @@ + + + diff --git a/components/date-picker/demo/cases/demo1.vue b/components/date-picker/demo/cases/demo1.vue new file mode 100644 index 00000000..c960af10 --- /dev/null +++ b/components/date-picker/demo/cases/demo1.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/components/date-picker/demo/cases/demo2.vue b/components/date-picker/demo/cases/demo2.vue new file mode 100644 index 00000000..cd81e51e --- /dev/null +++ b/components/date-picker/demo/cases/demo2.vue @@ -0,0 +1,34 @@ + + + diff --git a/components/date-picker/demo/cases/demo3.vue b/components/date-picker/demo/cases/demo3.vue new file mode 100644 index 00000000..8c705fd4 --- /dev/null +++ b/components/date-picker/demo/cases/demo3.vue @@ -0,0 +1,76 @@ + + + \ No newline at end of file diff --git a/components/date-picker/demo/index.vue b/components/date-picker/demo/index.vue new file mode 100644 index 00000000..4f67141d --- /dev/null +++ b/components/date-picker/demo/index.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/components/date-picker/index.vue b/components/date-picker/index.vue new file mode 100644 index 00000000..ba40e2bb --- /dev/null +++ b/components/date-picker/index.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/components/date-picker/test/index.spec.js b/components/date-picker/test/index.spec.js new file mode 100644 index 00000000..d8c7ff54 --- /dev/null +++ b/components/date-picker/test/index.spec.js @@ -0,0 +1,93 @@ +import DatePicker from '../index' +import {mount} from 'avoriaz' + +describe('DatePicker', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a date picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + isView: true, + minDate: new Date('2013/9/9'), + maxDate: new Date('2020/9/9'), + defaultDate: date, + }, + }) + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(3) + setTimeout(() => { + expect(eventStub.calledWith('initialed')).to.be.true + expect(wrapper.instance().getFormatDate('yyyy-MM-dd')).to.equal( + `${date.getFullYear()}-${date.getMonth() + 1 < 10 + ? '0' + (date.getMonth() + 1) + : date.getMonth() + 1}-${date.getDate() < 10 ? '0' + date.getDate() : date.getDate()}`, + ) + done() + }, 50) + }) + }) + + it('create a time picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + type: 'time', + unitText: ['', '', '', 'h', 'm'], + halfDayText: ['AM', 'PM'], + isView: true, + isTwelveHours: true, + defaultDate: date, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(3) + // setTimeout(() => { + done() + // }, 0) + }) + }) + + it('create a datetime picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + type: 'datetime', + isView: true, + defaultDate: date, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(5) + // setTimeout(() => { + done() + // }, 0) + }) + }) + + it('create a custom picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + type: 'custom', + value: true, + customTypes: ['dd', 'hh', 'mm'], + isTwelveHours: true, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(3) + wrapper.vm.isPickerShow = false + done() + }) + }) +}) diff --git a/components/dialog/README.md b/components/dialog/README.md new file mode 100644 index 00000000..30d1132e --- /dev/null +++ b/components/dialog/README.md @@ -0,0 +1,93 @@ +--- +title: Dialog 模态窗 +preview: https://didi.github.io/mand-mobile/examples/dialog +--- + +交互式模态窗口 + +### 引入 + +```javascript +import { Dialog } from 'mand-mobile' + +Vue.component(Dialog.name, Dialog) +``` + +### 代码演示 + + +### API + +#### Dialog Props +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +| v-model | 双向绑定是否显示窗口 | Boolean | `false`|-| +| title | 窗口标题 | String | -|-| +| icon | Icon组件图标名称 | String | -|如需自定义图标, 请查看`Icon`组件| +| closable | 是否显示关闭按钮 | Boolean | `true`|-| +| btns | 底部操作按钮组 | Array | `[]`|-| +| append-to | 组件的挂载节点 | HTMLElement | `document.body`|-| +| has-mask | 是否有蒙层 | Boolean | `true`|-| +| mask-closable | 点击蒙层是否可关闭弹出层 | Boolean | `false`|-| +| position | 弹出层位置, `center/top/bottom/left/right` | String | `center`|-| +| transition | 弹出层过度动画, `fade, slide-up/down/left/right` | String | `fade`|-| +| prevent-scroll | 是否禁止滚动穿透 | Boolean | false |-| +| prevent-scroll-exclude | 禁止滚动排除元素 | String | -|-| + +### Dialog Slots +组件子元素会被当做默认插槽内容使用,适合于不需要标题的自定义窗口内容的场景。 + +#### Dialog.close() +隐藏弹窗 + +#### Dialog.confirm(props) +静态方法创建确认模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| icon | 图标 | String | -| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| cancelText | 底部取消按钮文字 | String | `取消`| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + +#### Dialog.alert(props) +静态方法创建警告模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| icon | 图标 | String | -| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + +#### Dialog.succeed(props) +静态方法创建成功确认模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + +#### Dialog.failed(props) +静态方法创建失败确认模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + + +#### Dialog Events + +##### @show() +模态窗口显示后触发的事件 + +##### @hide() +模态窗口隐藏后触发的事件 diff --git a/components/dialog/component.js b/components/dialog/component.js new file mode 100644 index 00000000..1c1cfb3d --- /dev/null +++ b/components/dialog/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'dialog', + 'text': '模态窗口', + 'category': 'feedback', + 'description': '弹出式交互窗口', + 'author': 'liuxinyumichael' +} diff --git a/components/dialog/demo/cases/demo0.vue b/components/dialog/demo/cases/demo0.vue new file mode 100644 index 00000000..0d3fa33b --- /dev/null +++ b/components/dialog/demo/cases/demo0.vue @@ -0,0 +1,108 @@ + + + diff --git a/components/dialog/demo/cases/demo1.vue b/components/dialog/demo/cases/demo1.vue new file mode 100644 index 00000000..e71e2510 --- /dev/null +++ b/components/dialog/demo/cases/demo1.vue @@ -0,0 +1,55 @@ + + + diff --git a/components/dialog/demo/index.vue b/components/dialog/demo/index.vue new file mode 100644 index 00000000..81aecc38 --- /dev/null +++ b/components/dialog/demo/index.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/components/dialog/dialog.vue b/components/dialog/dialog.vue new file mode 100644 index 00000000..adb8733c --- /dev/null +++ b/components/dialog/dialog.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/components/dialog/index.js b/components/dialog/index.js new file mode 100644 index 00000000..affcaa39 --- /dev/null +++ b/components/dialog/index.js @@ -0,0 +1,86 @@ +import Vue from 'vue' +import Dialog from './dialog' +const DialogConstructor = Vue.extend(Dialog) + +const noop = function() {} + +const generate = function({title = '', icon = '', content = '', closable = false, btns = []}) { + const vm = new DialogConstructor({ + propsData: { + value: true, + title, + icon, + content, + closable, + btns, + }, + }).$mount() + + vm.$on('input', val => { + if (!val) { + vm.value = false + } + }) + vm.$on('hide', () => { + vm.$destroy() + document.body.removeChild(vm.$el) + }) + + return vm +} + +Dialog.confirm = ({title = '', icon = '', content = '', cancelText = '取消', confirmText = '确定', onConfirm = noop}) => { + const vm = generate({ + title, + icon, + content, + btns: [ + { + text: cancelText, + handler: () => vm.close(), + }, + { + text: confirmText, + handler: () => { + if (onConfirm() !== false) { + vm.close() + } + }, + }, + ], + }) + + return vm +} + +Dialog.alert = ({title = '', icon = '', content = '', confirmText = '确定', onConfirm = noop}) => { + const vm = generate({ + title, + icon, + content, + btns: [ + { + text: confirmText, + handler: () => { + if (onConfirm() !== false) { + vm.close() + } + }, + }, + ], + }) + + return vm +} + +Dialog.succeed = props => { + props.icon = 'circle-right' + return Dialog.confirm(props) +} + +Dialog.failed = props => { + props.icon = 'circle-cross' + return Dialog.confirm(props) +} + +export default Dialog diff --git a/components/dialog/test/index.spec.js b/components/dialog/test/index.spec.js new file mode 100644 index 00000000..d328f006 --- /dev/null +++ b/components/dialog/test/index.spec.js @@ -0,0 +1,103 @@ +import Dialog from '../dialog.vue' +import sinon from 'sinon' +import {mount} from 'avoriaz' +// import { setTimeout } from 'timers'; + +describe('Dialog', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple dialog', () => { + wrapper = mount(Dialog, { + propsData: { + appendTo: false, + }, + }) + + expect(wrapper.hasClass('md-dialog')).to.equal(true) + }) + + it('create a simple dialog and append to body', function() { + wrapper = mount(Dialog, { + propsData: { + appendTo: document.body, + }, + }) + + expect(wrapper.vm.$el.parentNode).to.equal(document.body) + }) + + it('has custom title', () => { + wrapper = mount(Dialog, { + propsData: { + title: 'Dialog Title', + }, + }) + + expect(wrapper.first('.md-dialog-title').text()).to.equal('Dialog Title') + }) + + it('has custom content', () => { + wrapper = mount(Dialog, { + propsData: { + content: 'Lorem ipsum dolor sit amet.', + }, + }) + + expect(wrapper.first('.md-dialog-body div').text()).to.equal('Lorem ipsum dolor sit amet.') + }) + + it('has custom icon', () => { + wrapper = mount(Dialog, { + propsData: { + icon: 'circle-right', + }, + }) + + expect(wrapper.contains('.md-icon-circle-right')).to.equal(true) + }) + + it('handle button action click event', () => { + const clickHandler = sinon.stub() + wrapper = mount(Dialog, { + propsData: { + btns: [ + { + text: 'Cancel', + handler: null, + }, + { + text: 'Confirm', + handler: clickHandler, + }, + ], + }, + }) + + wrapper.find('.md-dialog-actions a')[0].trigger('click') + wrapper.find('.md-dialog-actions a')[1].trigger('click') + expect(clickHandler.called).to.equal(true) + }) + + // it('emit input/hide/show event', (done) => { + // wrapper = mount(Dialog, { + // propsData: { + // value: true + // } + // }) + // const eventStub = sinon.stub(wrapper.vm, '$emit') + // setTimeout(() => { + // expect(eventStub.calledWith('show')).to.be.true + // wrapper.first('.md-dialog-close').trigger('click') + // wrapper.setProps({ value: false }) + // setTimeout(() => { + // expect(eventStub.calledWith('input')).to.be.true + // expect(eventStub.calledWith('hide')).to.be.true + // done() + // }, 500) + // }, 500) + // }) +}) diff --git a/components/drop-menu/README.md b/components/drop-menu/README.md new file mode 100644 index 00000000..915ad486 --- /dev/null +++ b/components/drop-menu/README.md @@ -0,0 +1,66 @@ +--- +title: DropMenu 下拉菜单 +preview: https://didi.github.io/mand-mobile/examples/drop-menu +--- + +下拉菜单可用于列表筛选 + +### 引入 + +```javascript +import { DropMenu } from 'mand-mobile' + +Vue.component(DropMenu.name, DropMenu) +``` + +### 代码演示 + + +### API + +#### DropMenu Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|data|数据源|Array<{text, disabled, options, ...}>|`[]`|`disabled`为是否禁用,`options`类型为`Array<{text, disabled, ...}>`| +|defaultValue|初始值|Array|`[]`|-| +|option-render|返回各选项渲染内容|Function({text, disabled, ...}):String|-|`vue 2.1.0+`可使用`slot-scope`,参考`Radio`| + +#### DropMenu Methods + +##### getSelectedValue(index): listItem +获取某菜单项选中值 + +|参数 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|index|菜单项索引值|Number|-| + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|listItem|选项数据|Object: {text, disabled, options, ...}| + +##### getSelectedValues(): listItems +获取所有菜单项选中值 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|listItems|选项数据|Array<{text, disabled, options, ...}>| + +#### DropMenu Events + +##### @change(barItem, listItem) +选中某项事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|barItem|菜单项数据|Object: {text, disabled, options, ...}| +|listItem|选项数据|Object: {text, disabled, ...}| + +##### @show() +下拉菜单展示事件 + +##### @hide() +下拉菜单隐藏事件 diff --git a/components/drop-menu/component.js b/components/drop-menu/component.js new file mode 100644 index 00000000..878511da --- /dev/null +++ b/components/drop-menu/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'drop-menu', + 'text': '下拉菜单', + 'category': 'basic', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/drop-menu/demo/cases/demo0.vue b/components/drop-menu/demo/cases/demo0.vue new file mode 100644 index 00000000..a5811898 --- /dev/null +++ b/components/drop-menu/demo/cases/demo0.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/components/drop-menu/demo/cases/demo1.vue b/components/drop-menu/demo/cases/demo1.vue new file mode 100644 index 00000000..f4559e97 --- /dev/null +++ b/components/drop-menu/demo/cases/demo1.vue @@ -0,0 +1,66 @@ + + + \ No newline at end of file diff --git a/components/drop-menu/demo/cases/demo2.vue b/components/drop-menu/demo/cases/demo2.vue new file mode 100644 index 00000000..cdf4a263 --- /dev/null +++ b/components/drop-menu/demo/cases/demo2.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/components/drop-menu/demo/cases/demo3.vue b/components/drop-menu/demo/cases/demo3.vue new file mode 100644 index 00000000..47485528 --- /dev/null +++ b/components/drop-menu/demo/cases/demo3.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/components/drop-menu/demo/index.vue b/components/drop-menu/demo/index.vue new file mode 100644 index 00000000..a6ee35cf --- /dev/null +++ b/components/drop-menu/demo/index.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/components/drop-menu/index.vue b/components/drop-menu/index.vue new file mode 100644 index 00000000..db91744e --- /dev/null +++ b/components/drop-menu/index.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/components/drop-menu/test/index.spec.js b/components/drop-menu/test/index.spec.js new file mode 100644 index 00000000..8709f141 --- /dev/null +++ b/components/drop-menu/test/index.spec.js @@ -0,0 +1,193 @@ +import DropMenu from '../index' +import {mount} from 'avoriaz' + +describe('DropMenu', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple drop-menu', done => { + wrapper = mount(DropMenu) + expect(wrapper.hasClass('md-drop-menu')).to.be.true + expect(wrapper.vm.data.length).to.equal(0) + + wrapper.vm.data = [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ] + + wrapper.vm.$nextTick(() => { + const barItem = wrapper.find('.bar-item') + expect(barItem.length).to.equal(1) + expect(barItem[0].text()).to.equal('hello') + done() + }) + }) + + it('drop-menu bar item click', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ], + }, + }) + const barItem = wrapper.find('.bar-item') + const mockData = [{text: 'world'}] + + barItem[0].trigger('click') + expect(barItem[0].hasClass('active')).to.true + expect(wrapper.instance().isPopupShow).to.true + expect(JSON.stringify(wrapper.instance().activeMenuListData)).to.equal(JSON.stringify(mockData)) + + wrapper.vm.$nextTick(() => { + const listItem = wrapper.find('.md-radio-item') + expect(listItem.length).to.equal(1) + barItem[0].trigger('click') + done() + }) + }) + + it('drop-menu list item click', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ], + }, + }) + const barItem = wrapper.find('.bar-item') + const mockData = [{text: 'world'}] + + barItem[0].trigger('click') + wrapper.vm.$nextTick(() => { + const listItem = wrapper.find('.md-radio-item') + listItem[0].trigger('click') + + expect(JSON.stringify(wrapper.instance().selectedMenuListItem)).to.equal(JSON.stringify(mockData)) + expect(barItem[0].text()).to.equal('world') + expect(wrapper.instance().getSelectedValue(0).text).to.equal('world') + expect(wrapper.instance().getSelectedValues()[0].text).to.equal('world') + done() + }) + }) + + it('drop-menu events', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ], + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const barItem = wrapper.find('.bar-item')[0] + + barItem.trigger('click') + expect(wrapper.vm.isPopupShow).to.equal(true) + expect(wrapper.vm.activeMenuBarIndex).to.equal(0) + setTimeout(() => { + const listItem = wrapper.find('.md-radio-item')[0] + listItem.trigger('click') + expect(eventStub.calledWith('change')).to.be.true + setTimeout(() => { + done() + }, 500) + }, 500) + }) + + it('create a disabled drop-menu', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + disabled: true, + }, + { + text: 'hello', + options: [ + { + text: 'hello', + disabled: true, + }, + ], + }, + ], + }, + }) + const barItem = wrapper.find('.bar-item') + expect(barItem[0].hasClass('disabled')).to.true + + barItem[1].trigger('click') + setTimeout(() => { + const listItem = wrapper.find('.md-radio-item') + expect(listItem[0].hasClass('disabled')).to.true + done() + }, 500) + }) + + it('create a drop-menu with defult value', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + { + text: 'space', + }, + ], + }, + ], + defaultValue: ['space'], + }, + }) + + wrapper.vm.$nextTick(() => { + const barItem = wrapper.find('.bar-item') + expect(barItem[0].hasClass('selected')).to.true + expect(barItem[0].text()).to.equal('space') + + barItem[0].trigger('click') + setTimeout(() => { + const listItem = wrapper.find('.md-radio-item') + expect(listItem[1].hasClass('selected')).to.true + done() + }, 300) + }) + }) +}) diff --git a/components/field-item/index.vue b/components/field-item/index.vue new file mode 100644 index 00000000..8d2e645c --- /dev/null +++ b/components/field-item/index.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/components/field-item/test/index.spec.js b/components/field-item/test/index.spec.js new file mode 100644 index 00000000..5582e98c --- /dev/null +++ b/components/field-item/test/index.spec.js @@ -0,0 +1,87 @@ +import FieldItem from '../index' +import {mount} from 'avoriaz' + +describe('FieldItem', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple field-item', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + }, + }) + + expect(wrapper.hasClass('md-field-item')).to.be.true + expect(wrapper.vm.title).to.equal('field item title') + }) + + it('create a simple field-item with brief', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + }, + }) + + expect(wrapper.find('.md-field-item-brief').length).to.equal(1) + }) + + it('create a simple field-item with arrow', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + arrow: 'arrow-right', + }, + }) + + expect(wrapper.hasClass('has-arrow')).to.be.true + + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.trigger('click') + expect(eventStub.calledWith('click')).to.be.true + }) + + it('create a field-item with solid title', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + arrow: 'arrow-right', + solid: true, + }, + }) + + expect(wrapper.find('.solid').length).to.equal(1) + }) + + it('create a field-item with customized value align right', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + customized: true, + align: 'right', + }, + }) + + expect(wrapper.vm.customized).to.be.true + }) + + it('create a disabled field-item', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + arrow: 'arrow-right', + disabled: true, + }, + }) + + expect(wrapper.hasClass('disabled')).to.be.true + }) +}) diff --git a/components/field/README.md b/components/field/README.md new file mode 100644 index 00000000..dcae32d3 --- /dev/null +++ b/components/field/README.md @@ -0,0 +1,47 @@ +--- +title: Field 区域列表组合 +preview: https://didi.github.io/mand-mobile/examples/field +--- + +区域列表垂直排列,显示当前的内容、状态和可进行的操作 + +### 引入 + +```javascript +import { Field, FieldItem } from 'mand-mobile' + +Vue.component(Field.name, Field) +Vue.component(FieldItem.name, FieldItem) +``` + +### 代码演示 + + +### API + +#### Field Props +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +|title|标题|String|-|-| + +#### FieldItem Props +|属性 | 说明 | 类型 | 默认值 |备注| +|----|-----|------|------|------| +|name|标识|Number/String| `-1`|-| +|title|标题|String|-|-| +|brief|子标题|String|-|-| +|disabled|是否禁用|Boolean|`true`|-| +|arrow|箭头名称|String|-|`arrow-up`, `arrow-right`, `arrow-down`, `arrow-left`| +|customized|内容是否自定义|Boolean|是否有`slot`|-| +|align|自定义内容时,内容位置|String|`left`|`left`, `right`, `center`| +|value|内容|String|-|-| +|solid|是否固定标题宽度,超出会自动换行|Boolean|`false`|-| + +#### FieldItem Events + +##### @click(name) +点击事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|name|fieldItem标识|Number/String| diff --git a/components/field/component.js b/components/field/component.js new file mode 100644 index 00000000..f6f65d3a --- /dev/null +++ b/components/field/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'field', + 'text': '组合列表', + 'category': 'form', + 'description': '单个连续模块垂直排列,显示当前的内容、状态和可进行的操作', + 'author': 'chengyanjing' +} diff --git a/components/field/demo/cases/demo0.vue b/components/field/demo/cases/demo0.vue new file mode 100644 index 00000000..646595aa --- /dev/null +++ b/components/field/demo/cases/demo0.vue @@ -0,0 +1,62 @@ + + + + + \ No newline at end of file diff --git a/components/field/demo/cases/demo1.vue b/components/field/demo/cases/demo1.vue new file mode 100644 index 00000000..923ec975 --- /dev/null +++ b/components/field/demo/cases/demo1.vue @@ -0,0 +1,72 @@ + + + + + \ No newline at end of file diff --git a/components/field/demo/cases/demo2.vue b/components/field/demo/cases/demo2.vue new file mode 100644 index 00000000..dd0428ab --- /dev/null +++ b/components/field/demo/cases/demo2.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/components/field/demo/index.vue b/components/field/demo/index.vue new file mode 100644 index 00000000..e61028da --- /dev/null +++ b/components/field/demo/index.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/components/field/index.vue b/components/field/index.vue new file mode 100644 index 00000000..0ec86e91 --- /dev/null +++ b/components/field/index.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/components/field/test/index.spec.js b/components/field/test/index.spec.js new file mode 100644 index 00000000..815099a1 --- /dev/null +++ b/components/field/test/index.spec.js @@ -0,0 +1,29 @@ +import Field from '../index' +import {mount} from 'avoriaz' + +describe('Field', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple field with title', () => { + wrapper = mount(Field, { + propsData: { + title: 'field title', + }, + }) + expect(wrapper.hasClass('md-field')).to.be.true + expect(wrapper.vm.title).to.equal('field title') + }) + + it('create a simple field without title', () => { + wrapper = mount(Field, { + propsData: { + title: '', + }, + }) + expect(wrapper.find('.md-field-title').length).to.equal(0) + }) +}) diff --git a/components/icon/README.md b/components/icon/README.md new file mode 100644 index 00000000..8538d1ce --- /dev/null +++ b/components/icon/README.md @@ -0,0 +1,82 @@ +--- +title: Icon 图标 +preview: https://didi.github.io/mand-mobile/examples/icon +--- + +SVG 图标 + +### 引入 + +```javascript +import { Icon } from 'mand-mobile' + +Vue.component(Icon.name, Icon) +``` + +### 使用指南 + +组件库内置图标可直接使用,`arrow-up/down/left/right`, `circle-alert/cross/right`,`hollo-plus`,`cross`,`spinner` + +其他自定义图标需使用svg-sprite-loader,svg文件名即图标名称 + +1. 安装依赖 + +```shell +npm install svg-sprite-loader --save-dev +``` + +2. webpack配置 + +```javascript +const path = require('path') + +module.exports = { + module: { + loaders: [ + { + test: /\.svg$/i, + loader: 'svg-sprite-loader', + include: [ + // 将某个路径下所有svg交给 svg-sprite-loader 插件处理 + path.resovle(__dirname, 'src/my-project-svg-folder') + ], + } + ] + } +} +``` +3. 引入图标 + +```vue + + + +``` + +### 代码演示 + + +### API + +#### Icon Props +|属性 | 说明 | 类型 | 默认值| 备注| +|----|-----|------|------|------| +|name|图标名称|String|-|-| +|size|图标大小|String|`md`|`xs`, `sm`, `md`, `lg`| +|color|图标颜色|String|`currentColor`|该颜色值会作为`fill`的值被设置在`svg`图标上| diff --git a/components/icon/default-svg-list.js b/components/icon/default-svg-list.js new file mode 100644 index 00000000..70d3c466 --- /dev/null +++ b/components/icon/default-svg-list.js @@ -0,0 +1,26 @@ +// import '../_style/images/hollow-plus.svg' +// import '../_style/images/arrow-up.svg' +export default { + 'hollow-plus': '', + + 'arrow-up': '', + + 'arrow-down': '', + + 'arrow-left': '', + + 'arrow-right': '', + + 'cross': '', + 'circle-alert': '', + + 'circle-cross': '', + + 'circle-right': '', + + 'spinner': '', + + 'right': '', + + 'circle': '' +} diff --git a/components/icon/demo/cases/demo0.vue b/components/icon/demo/cases/demo0.vue new file mode 100644 index 00000000..ca1a8563 --- /dev/null +++ b/components/icon/demo/cases/demo0.vue @@ -0,0 +1,59 @@ + + + diff --git a/components/icon/demo/cases/demo1.vue b/components/icon/demo/cases/demo1.vue new file mode 100644 index 00000000..43ba53d8 --- /dev/null +++ b/components/icon/demo/cases/demo1.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/icon/demo/cases/demo2.vue b/components/icon/demo/cases/demo2.vue new file mode 100644 index 00000000..4f4f3acf --- /dev/null +++ b/components/icon/demo/cases/demo2.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/icon/demo/index.vue b/components/icon/demo/index.vue new file mode 100644 index 00000000..6ad31ba5 --- /dev/null +++ b/components/icon/demo/index.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/components/icon/index.vue b/components/icon/index.vue new file mode 100644 index 00000000..67bccc9e --- /dev/null +++ b/components/icon/index.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/components/icon/load-spirte.js b/components/icon/load-spirte.js new file mode 100644 index 00000000..289b6c21 --- /dev/null +++ b/components/icon/load-spirte.js @@ -0,0 +1,40 @@ +// inspried by https://github.com/kisenka/svg-sprite-loader/blob/master/runtime/browser-sprite.js +// Much simplified, do make sure run this after document ready +import icons from './default-svg-list' + +const svgSprite = contents => ` + + + ${contents} + + +` + +const renderSvgSprite = () => { + const symbols = Object.keys(icons) + .map(iconName => { + const svgContent = icons[iconName].split('svg')[1] + return `` + }) + .join('') + return svgSprite(symbols) +} + +const loadSprite = () => { + if (!document) { + return + } + const existing = document.getElementById('__MAND_MOBILE_SVG_SPRITE_NODE__') + const mountNode = document.body + + if (!existing) { + mountNode.insertAdjacentHTML('afterbegin', renderSvgSprite()) + } +} + +export default loadSprite diff --git a/components/icon/test/index.spec.js b/components/icon/test/index.spec.js new file mode 100644 index 00000000..dca2f9f0 --- /dev/null +++ b/components/icon/test/index.spec.js @@ -0,0 +1,17 @@ +import Icon from '../index' +import {mount} from 'avoriaz' + +describe('Icon', () => { + let wrapper + + it('create a red icon', () => { + wrapper = mount(Icon, { + propsData: { + name: 'hollow-plus', + color: 'red', + }, + attachToDocument: true, + }) + expect(wrapper.hasStyle('fill', 'red')).to.be.true + }) +}) diff --git a/components/image-reader/README.md b/components/image-reader/README.md new file mode 100644 index 00000000..ef556a26 --- /dev/null +++ b/components/image-reader/README.md @@ -0,0 +1,71 @@ +--- +title: ImageReader 图片选择器 +preview: https://didi.github.io/mand-mobile/examples/image-reader +--- + +用于相册照片读取或拉起拍照 + +### 引入 + +```javascript +import { ImageReader } from 'mand-mobile' +import imageProcessor from 'mand-mobile/lib/image-reader/image-processor' // 图片处理插件,用法参考#imageProcessor + +Vue.component(ImageReader.name, ImageReader) +``` + +### 代码演示 + + +### API + +#### ImageReader Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|name|标识|String|-|可用于区分多个选择器| +|size|图片尺寸限制|String/Number|-|单位`kb`| +|mime|支持图片类型|Array|`*`|如`['jpeg','png']`| +|is-camera-only|是否只支持拍照|Boolean|`false`|-| +|is-multiple|是否支持选择多张|Boolean|`false`|-| +|amount|选择多张|Number|-|只在`is-multiple`为`true`时有效| + +#### ImageReader Events + +##### @select +图片选择完成事件,还未开始读取 + +##### @complete(name, { dataUrl, blob }) +图片选择读取完成事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|name|选择器标识|String| +|dataUrl|图片Base64|String| +|blob|图片Blob对象,可用于`formData`|Blob| + +### imageProcessor + +用于图片轴向修正,图片质量压缩,宽高控制 + +#### 引入 + +```javascript +import imageProcessor from 'mand-mobile/lib/image-reader/image-processor' + +/** + * options 图片处理配置 + * fn(dataUrl, blob) 处理完成回调 + * @return Promise({dataUrl, blob}) + */ +imageProcessor(options[, fn]) + +``` + +#### options + +|属性 | 说明 | 类型| 备注| +|----|-----|------|------| +|dataUrl|图片Base64|String|-| +|width|图片宽度|Number|单位`px`, 宽度超出时等比缩放| +|height|图片高度|Number|单位`px`, 高度超出时等比缩放| +|quality|图片质量|Number|取值范围`0-1`| diff --git a/components/image-reader/component.js b/components/image-reader/component.js new file mode 100644 index 00000000..12fddeed --- /dev/null +++ b/components/image-reader/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'image-reader', + 'text': '图片选择器', + 'category': 'basic', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/image-reader/demo/cases/demo0.vue b/components/image-reader/demo/cases/demo0.vue new file mode 100644 index 00000000..b9e45f14 --- /dev/null +++ b/components/image-reader/demo/cases/demo0.vue @@ -0,0 +1,123 @@ + + + + + \ No newline at end of file diff --git a/components/image-reader/demo/cases/demo1.vue b/components/image-reader/demo/cases/demo1.vue new file mode 100644 index 00000000..88806320 --- /dev/null +++ b/components/image-reader/demo/cases/demo1.vue @@ -0,0 +1,132 @@ + + + + + \ No newline at end of file diff --git a/components/image-reader/demo/index.vue b/components/image-reader/demo/index.vue new file mode 100644 index 00000000..d0e6530c --- /dev/null +++ b/components/image-reader/demo/index.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/components/image-reader/image-dataurl.js b/components/image-reader/image-dataurl.js new file mode 100644 index 00000000..db64e24b --- /dev/null +++ b/components/image-reader/image-dataurl.js @@ -0,0 +1,50 @@ +/** + * DataURI to ArrayBuffer + * @param {*} dataURI + */ +export function dataURIToArrayBuffer(dataURI) { + // 'data:image/jpeg;dataURI,...' => 'image/jpeg' + // contentType = contentType || dataURI.match(/^data:([^;]+);base64,/mi)[1] || '' + dataURI = dataURI.replace(/^data:([^;]+);base64,/gim, '').replace(/\s/g, '') + + const binary = atob(dataURI) + const len = binary.length + const buffer = new ArrayBuffer(len) + const view = new Uint8Array(buffer) + + for (let i = 0; i < len; i++) { + view[i] = binary.charCodeAt(i) + } + + return buffer +} + +/** + * Base64 to Blob + * @param {String} dataURI + */ +export function dataURItoBlob(dataURI) { + // convert base64/URLEncoded data component to raw binary data held in a string + let byteString + + if (dataURI.split(',')[0].indexOf('base64') >= 0) { + byteString = atob(dataURI.split(',')[1]) + } else { + byteString = unescape(dataURI.split(',')[1]) + } + + // separate out the mime component + const mimeString = dataURI + .split(',')[0] + .split(':')[1] + .split(';')[0] + + // write the bytes of the string to a typed array + const ia = new Uint8Array(byteString.length) + + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i) + } + + return new Blob([ia.buffer], {type: mimeString}) +} diff --git a/components/image-reader/image-processor.js b/components/image-reader/image-processor.js new file mode 100644 index 00000000..f0be61ae --- /dev/null +++ b/components/image-reader/image-processor.js @@ -0,0 +1,222 @@ +import {dataURIToArrayBuffer, dataURItoBlob} from './image-dataurl' +import {noop, requireRemoteScript} from '../_util' + +const UA = (userAgent => { + const isOldIos = /OS (\d)_.* like Mac OS X/g.exec(userAgent) + const isOldAndroid = /Android (\d.*?);/g.exec(userAgent) || /Android\/(\d.*?) /g.exec(userAgent) + + // IOS8.3- + // ndroid4.5- + // ios + // android + // QQ Browser + return { + oldIOS: isOldIos ? +isOldIos.pop() < 8.3 : false, + oldAndroid: isOldAndroid ? +isOldAndroid.pop().substr(0, 3) < 4.5 : false, + ios: /\(i[^;]+;( U;)? CPU.+Mac OS X/.test(userAgent), + android: /Android/g.test(userAgent), + mQQBrowser: /MQQBrowser/g.test(userAgent), + } +})(navigator.userAgent) + +/** +* Get Orientation of EXIF +* @param {Object} dataURL +* @souce http://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side +*/ +/* istanbul ignore next */ +function getOrientation(dataURL) { + const buffer = dataURIToArrayBuffer(dataURL) + const view = new DataView(buffer) + + if (view.getUint16(0, false) !== 0xffd8) { + return -2 + } + + const length = view.byteLength + let offset = 2 + + while (offset < length) { + const marker = view.getUint16(offset, false) + offset += 2 + + if (marker === 0xffe1) { + const tmp = view.getUint32((offset += 2), false) + + if (tmp !== 0x45786966) { + return -1 + } + + const little = view.getUint16((offset += 6), false) === 0x4949 + offset += view.getUint32(offset + 4, little) + + const tags = view.getUint16(offset, little) + offset += 2 + + for (let i = 0; i < tags; i++) { + if (view.getUint16(offset + i * 12, little) === 0x0112) { + return view.getUint16(offset + i * 12 + 8, little) + } + } + } else if ((marker & 0xff00) !== 0xff00) { + break + } else { + offset += view.getUint16(offset, false) + } + } + + return -1 +} +/* istanbul ignore next */ +function getImageSize(img, orientation, maxWidth, maxHeight) { + const ret = { + width: img.width, + height: img.height, + } + + if ('5678'.indexOf(orientation) > -1) { + ret.width = img.height + ret.height = img.width + } + + // 如果原图小于设定,采用原图 + if (ret.width < maxWidth || ret.height < maxHeight) { + return ret + } + + const scale = ret.width / ret.height + + if (maxWidth && maxHeight) { + if (scale >= maxWidth / maxHeight) { + if (ret.width > maxWidth) { + ret.width = maxWidth + ret.height = Math.ceil(maxWidth / scale) + } + } else { + if (ret.height > maxHeight) { + ret.height = maxHeight + ret.width = Math.ceil(maxHeight * scale) + } + } + } else if (maxWidth) { + if (maxWidth < ret.width) { + ret.width = maxWidth + ret.height = Math.ceil(maxWidth / scale) + } + } else if (maxHeight < ret.height) { + ret.width = Math.ceil(maxHeight * scale) + ret.height = maxHeight + } + + // 超过这个值base64无法生成,在IOS上 + if (ret.width >= 3264 || ret.height >= 2448) { + ret.width *= 0.8 + ret.height *= 0.8 + } + + return ret +} +/* istanbul ignore next */ +function makeCanvas(img, orientation, maxWidth, maxHeight, quality) { + const {width, height} = getImageSize(img, orientation, maxWidth, maxHeight) + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + canvas.width = width + canvas.height = height + ctx.drawImage(img, 0, 0, width, height) + + let base64 = null + switch (orientation) { + case 3: + ctx.rotate(180 * Math.PI / 180) + ctx.drawImage(img, -width, -height, width, height) + break + case 6: + ctx.rotate(90 * Math.PI / 180) + ctx.drawImage(img, 0, -width, height, width) + break + case 8: + ctx.rotate(270 * Math.PI / 180) + ctx.drawImage(img, -height, 0, height, width) + break + case 2: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.drawImage(img, 0, 0, width, height) + break + case 4: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.rotate(180 * Math.PI / 180) + ctx.drawImage(img, -width, -height, width, height) + break + case 5: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.rotate(90 * Math.PI / 180) + ctx.drawImage(img, 0, -width, height, width) + break + case 7: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.rotate(270 * Math.PI / 180) + ctx.drawImage(img, -height, 0, height, width) + break + default: + ctx.drawImage(img, 0, 0, width, height) + } + + if (UA.oldIOS || UA.oldAndroid || UA.mQQBrowser || !navigator.userAgent) { + /* global JPEGEncoder */ + const encoder = new JPEGEncoder() + const newImg = ctx.getImageData(0, 0, canvas.width, canvas.height) + base64 = encoder.encode(newImg, quality * 100) + } else { + base64 = canvas.toDataURL('image/jpeg', quality) + } + + return base64 +} +/** + * Image Process + * @param options Object: { dataUrl, width, height, quality} + * @param fn dataUrl => void + */ +export default function(options, fn = noop) { + return new Promise((resolve, reject) => { + const {dataUrl, width, height, quality} = options + const orientation = getOrientation(dataUrl) + const blob = dataURItoBlob(dataUrl) + /* istanbul ignore next */ + if (orientation > 1 || quality < 1 || width || height) { + const img = new Image() + img.src = dataUrl + img.onload = () => { + const newDataUrl = makeCanvas(img, orientation, width, height, quality) + const newBlob = dataURItoBlob(newDataUrl) + fn(newDataUrl, newBlob) + resolve({ + dataUrl: newDataUrl, + blob: newBlob, + }) + } + img.onerror = () => { + fn(null) + reject(new Error('image load error')) + } + } else { + fn(dataUrl, blob) + resolve({ + dataUrl, + blob, + }) + } + }) +} + +// Import jpeg_encoder_basic for compatibility if necessary +if (UA.oldIOS || UA.oldAndroid || UA.mQQBrowser || !navigator.userAgent) { + /* istanbul ignore next */ + requireRemoteScript('//manhattan.didistatic.com/static/manhattan/mfd/image-reader/jpeg_encoder_basic.js') +} diff --git a/components/image-reader/image-reader.js b/components/image-reader/image-reader.js new file mode 100644 index 00000000..b91cb1ce --- /dev/null +++ b/components/image-reader/image-reader.js @@ -0,0 +1,91 @@ +/** + * Read Image In Web Worker or main thread + * + * STATUS + * 0: success + * 100: 'browser does not support', + * 101: 'picture size is beyond the preset', + * 102: 'picture read failure', + * 103: 'the number of pictures exceeds the limit' + */ + +export default function(global) { + /** + * Constructor + * @param{*} [Array]files 图片文件 + * @param{*} [Boolean]isWebWorker 是否为webwork模式调用 + * @param{*} [Number]size 单张图片大小限制 + * @param{*} [Function]complete 非webwork模式时 回调 res { errorCode: '0', file, dataUrl } + */ + function ImageReader(options) { + /* istanbul ignore if */ + if (!options.files) { + return + } + + this.files = options.files + this.index = 0 + this.size = options.size || 0 + + if (!options.isWebWorker && options.complete) { + this.callback = options.complete + } + + this.readImage(options.files[this.index]) + } + + ImageReader.prototype.readImage = function(file) { + // iterator + const next = + this.files && this.index < this.files.length - 1 + ? () => { + this.index += 1 + this.readImage(this.files[this.index]) + } + : null + + const onReadImageComplete = msg => { + /* istanbul ignore else */ + if (this.callback) { + this.callback(msg) + } else { + postMessage(msg) + } + next && next() + } + + if (!this.size || file.size <= this.size) { + const reader = new FileReader() + + reader.onload = readerEvt => { + const dataUrl = readerEvt.target.result + onReadImageComplete({errorCode: 0, file, dataUrl}) + } + reader.onerror = () => { + /* istanbul ignore next */ + onReadImageComplete({errorCode: 102}) + } + + reader.readAsDataURL(file) + } else { + onReadImageComplete({errorCode: 101}) + } + } + + const onmessageCallback = function(workerEvt) { + const imageReader = new ImageReader(workerEvt.data) + return imageReader + } + + if (global) { + return function(data) { + return onmessageCallback({data}) + } + } else { + /* global onmessage */ + /* eslint no-unused-vars: 0 */ + /* eslint no-global-assign: 0 */ + /* istanbul ignore next */ + onmessage = onmessageCallback + } +} diff --git a/components/image-reader/index.vue b/components/image-reader/index.vue new file mode 100644 index 00000000..b8e54d75 --- /dev/null +++ b/components/image-reader/index.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/components/image-reader/test/file.mock.js b/components/image-reader/test/file.mock.js new file mode 100644 index 00000000..4024f319 --- /dev/null +++ b/components/image-reader/test/file.mock.js @@ -0,0 +1,18 @@ +const _dataUrl = + 'data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAeAAD/4QMpaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjAtYzA2MCA2MS4xMzQ3NzcsIDIwMTAvMDIvMTItMTc6MzI6MDAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFN0FDRjU4MzJDOTUxMUU2ODU3NUFENDA4MDE5NDRGRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFN0FDRjU4NDJDOTUxMUU2ODU3NUFENDA4MDE5NDRGRSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU3QUNGNTgxMkM5NTExRTY4NTc1QUQ0MDgwMTk0NEZFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU3QUNGNTgyMkM5NTExRTY4NTc1QUQ0MDgwMTk0NEZFIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4AIUFkb2JlAGTAAAAAAQMAEAMCAwYAAAj7AAAOogAAH1b/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIASwBLAMBIgACEQEDEQH/xAC/AAEAAgMBAQAAAAAAAAAAAAAAAQIFBgcDBAEBAQEBAAAAAAAAAAAAAAAAAAECAxAAAQMDAwMEAgEFAQEAAAAAAQARAgMEBSESBhAgMTAiExZBFEBQYIAVBzIzEQABAgQDBQUCCQoFBQEAAAABEQIAITEDQVESYXGBIgSRobEyE8HREDBA8OFCsiMUIPFSYnKCktI0BYCiwjNTQ2NzgyREEgABBAICAwEAAAAAAAAAAAABACAwMWAREIFwIVFx/9oADAMBAAIRAxEAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB6RNdTcxudLczk6W5vY6Mra5EEtZxsu8Oe+S9Hc59DoTQvsNxVtcgAAAAAAAa3z7oWgZ6Vi8TULiMni9is6CxuR1ymJHIPP7Pkx2iLQVkEWg6fldT2zfIEAAAAAAFTTNU+r5s9q/X82Rzq77Yzr4fDIfOnwdP5r07fL6Bvnz3A7TquOs1mGotBCYXP9D5P1fXORcAAAAAAMDntFmtaGes/f8EGbYSM3M+OMg+rqPI+o755E+TXPQsPDHaJQCCZrKus8l6PcZwa5gAAAAAOZ9K5PN+U3Z35hUTBWZhI3XS/vs6lp238ouPlmYnRMTFZCJi5Tc9OzdnRhrkAAAAABjuZdXw83oMb/wCUuht7qaNO6+a6Y3GU02+1fMu7ck3rXbnDtm9JrVo2oapXbBqs7TJq3rscm7zS+uQAAAAAGMxOJ16dN5nRqy73OhSm7+Om0Oi+nJ/quehah4ek28aF2/c+NeDHbcfx/wBLOxW4/K9gnj8nXrchk7DOBz1yCAAAAa9qf369npbzXarWUVWgik0qlqXT0+n5fol8oRXjS9Ga+vn6F5iVTEraYGZ2XRITr3prmx65ggAA8zmXwIz2mQqIqvQr53pZWa2PT6vj3A1OOlYtNE8fp+Ym9PQusWCyr0sTMDZd95f1C8wuQAGOyOCXncwz1WrYgrF4gRS9KqtKfb1T5vs1zBOeap23l03h7XTSLytJBalibVmLdc5B0/WMmLgABqO3c+msBats9ETBaiCUCIVJTJ1n6fP03xAc06XzmawKLTpFoCUEWqLwiLbxo202byNcgAGrbSXlleqprlVerjlLqw5VTrA5O6wOTevVB5+hcAOe9C+NeTz0m81zO3TrnLY6vaOSuuWrkN+vSci2/b5QLkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/2gAIAQIAAQUA/rpKdOE46uE4ThOPSPnoO8eiUfHSHQ+eo9A9CHW1bVHuHoHtCPaPPeQmTFMUyZFMmKYpimPoOnKcpynRKdAp06dP/Ef1W7x2H0T3DsPj0D3Dz2MmTJkybsZMmTJk39n/AP/aAAgBAwABBQD+uiJK2FbCth67SthWwrafSh46Hx3Ea+hEMB56VOkfHWfnviNejp1U6Adk/HfAJ+whxAa9ktR3xkAN4W8LeFuC3BRcLcFuC3Bbgt0fQEQ2wLaFtCEUAAGUo6iK2LYtiIY9sQG6noPCPnsMQUQx9A9HAjvC/PbPz1Hnt/JPSOvdPsh59CHntl46gst5W8reVvK3lbz1BZbwt63ret6MnH9nf//aAAgBAQABBQD/AAS5dfXFjiByLOKjyfOwq3fJ8rUvI8q5DCP27kDQ5lyCI+38gnOG7b1u+c46jKpz68Iq83zcl91zoX3XOgfeM2BS57kwqX/QKYEJxqQ/g86i+F/IkQKf/wAkYo7Vxqy/bzXU+LgCNx4A1GgILnXd+GccYujdYP8Ag85m2H2hMyESIinUfbJm14jksXjKtLkWEqoEEdMrR+HJMHZEIBk2ngeDwG63Wv8AB55didaQKYAWMBUkaLI0tJUCpU2U6bDEBsV05nbxo5kPJNodR5DJijEyXCrk0c1/AnONOGSvZ318XJYLFuYmWvuJkKTylFq4Pw2L/pdOet+35RPtO0RBYuNwZvziq4tsl/A5hfm1xUggPczrGV6VKHzW5BnSkpbVOOtxpCzlutOnLLz9zNGLEBAe5pMPIdOvAx1x+1Yevzq5E8iSXcMAwchAudNwiiUDJsBX/Ywyyl7HH4+c5zn+W0JcDz7gHQ8hcOuPmwXr8lrC4zf5Oo1ifES5R3PuJWoUjrwXIxnQXO8g61T6op9PztILSKiAFwG6Bj61SYp061T5qsKUJicYANApniNxQEgGTFiCVgLqdpmJSEY5S8nf3w9q1CYAiOpAALFEziBTM1KM4nhtx8Gc9bP1/wBfDB1ECKjqTJ4vJidwkYuJSIJ93vAtpmFxeTMLKo4lEyJEmQJC0BOkt0inUZyAlVqSVjXla3oII9W7tLe9t5cMwxI4XhwZcHxZX0bGueC45jwO0KPAaZR4AGHAIAS/5/UevxvG4ypV5zhyJUeN152vDo3tL6Der6HfgHgV6R9Bvl9Bv1HgN8B9BvWlwK/VPgN2Z0ofHS9XM521w8fvlihzqwb73jdx53jAhznFlT57ZhR/6BAVKPOeN1hLmXHoxuucWey+qG4ritalV6gMuCX1tRiJRIRz+FjWhksdUH71iv3rJC7tSv3rJftWxPrc1uTWy/uAkCpak7jEORIEKUXhSpTpyiVaiPyXJc7tCzzc0RUq0xWv70Kg/wATgyEg8ZAAMTucblw69N1h/T5dmLnG20+S517irUrVtw3EMS7nQuIgkNIOJSaAJJsZz/2Fy25vcQ6u57LYSLGMXjJDySAtUdAPAJA49n54WrDneMJt69K5o+jzmv8ALlAQACXckl5ptQSUxip6EsTqI6bscQclVINWEQC8yL9v1g+1yBCLrRyEPAOhYHc6cEMW4RfC4xPo8huPmzEZbIbUAQZNE6OTtW9kdVI+0iQADLFgnIVWM47ZAnS9A/UgXgWaDiRYCMnluKGvSOh/9LcuCXMqeS9CtUFGjWqfJUkEZAqJi0nepI7o+0EAgFpS8SlpF3xc/jrYPiX+wtvp/HmyXAqEqd5Qq0KFN/iHmmCE4UQCgCY+U4RDCYIk4bjdwKGc9DP1xQwwgpAglgWCk5LPEEAnY8oiJaMjsQgXxnGc3cSo040aXTn9hcU72nCXwxgVGCiCQQ0fyGAiQCEDBOQKFQ0KtKpGrT7+ZVfjwMYkCOpDkMx03SBjEglS0Et5RGgEkIusOBHFdSARySlGnnIs4coSKAAW4kD3B2DstzICJRBXHbsXeG7+e3DUYhCMdsS8jAxjKMtxiU0UfLEEHWI0CxwbH9nLg2ckdD5OgY7wCCRIEykxACIIkSCSCVwKtGWN7+aVpV8yCBTG5O63iJjKAiNzQACMtCHW4CQaMQFbxEaHZzSAGbDGBDk+9ExAIJk+ugW/eBL5AQQPc3A6sxkO/lXG619KWIzEDHE5MR/199ERx2Qkhi8oYyxWWMRisoZyxGTgKuLypIxuSmjiMoFQw2XnKjGcaPZzqO3L+1/c8JSES4LFAEgRiDq8STLQRhR+Q8Ixt3Srfyb/ABOPyUfp/HXjxXj8VHjeCjGOAwkZDD4kE4zGlCwsQI2dpE/FTXx00IxH+G3/2gAIAQICBj8Ayk+LrxL/2gAIAQMCBj8Axowhg4EgaONSEu38aZjsMtXB7b6ZatWtSdwaj7PHUQh/A7cvUlQ0qVKlrD//2gAIAQEBBj8A/wACRf01w2rty4xge3zATcU/hhfxt7+I14wx7+re5jSC5pQqBUeWL3UdL1dxli4S+1bKANYfLylQJQE6sn9pjD3lsBOqX9y3/LABuscazttVOAhjRdaCSEDbbZ9xhuvzINSUXH8gs6ezc6gtKapMbwVT3QtrpLbAv1i53hpghvosnVrCSBs1OMf7jM/I2PPbOE7Y7khEsk56D/NA9SxZeMUDmn7RgfiOjIVFNt4dVcCB4w17SrXAOacwZ/Imn9G+wpwcIQlE4zWK4Ti3L6rRwSBVfmsBPdEkGRjp2Vbbf6ryMBb55rmQn5NxhkWvIQbCYzOOzCJ8uZwEbDXxiUeGETKEzJwhJ81FjpHkq5jfTctVtnTPgPkTGpN95s9zXGDI0rko+mGla1hrCpSQEHlK5kRpLTkSRGOKBKfTHUXeuuOt3ntDbZ0lw0eZ3lByEDT1lsL+kdH20gEFQaH4eqtogbdeEGQcUmY/VGGUSSXGOzxim8pkYEp5HHH2RsHGfzESwkd2FI6rpTW29txq5Pani35F0/RA/wC203X5K/lb3AxtJjSp4SwhxIo0S3mEPAAwASVzmghZ7/ojHcsOzCIs6mOjFEs2/sj4S9oT1mNuFM/J/pgJwEAqhWkBK+MESmCibfzwCZLUiFpODNTScoFoyb1Ftzf3hzj7PyFz3lGtBc4nACZi/wBY9R6zyWjJo5WjgEhMcUEBRsG1JisX3eUgtaoC55QgfhQNJnkZGcFrXh5aFIA9qJxgEPGpoJe5DXBC4QDqaSPMd+FILyQQSBLZujp1CH0mKP3R8PSjH0yTmmqCqIDIQQZAYCJ4ZCClE+mJ8wTZlCmQND898TCe+kdL1BKC3dY44BC4A/ITZYUf1TvS26EJf7oWjpLlOApTT24QFCTXYU9kXWPuBhLgVOQ1CRhBdYgrzRpF22cxrbhnOBzsTDmFYRQmSwWuoXNTcBhFh0hqtsMqTaPhvBpVnTgWWIcW+b/MTEp7vpg8IA3AxmahI0upitIGJOMAmeJ3JKJkjEmOm6g1u2mOO8ifyDp+m1ANs29Rw5rhSfARiD3ZwMNUwZUX2QdUziq02R78vmYdQVIwgjYEP54QlGjiZQVIE13z2QioqyPCsdHdx9INO9nL7Pgv9W7/AKTTpGbjytHaYL3uW49XOLsSZqYWeHbLCKIkKMMN0DBCk55iCMaccawSQMxsNYQJkUoolAcDgqItJxaaSpsufbPbqHc75B1TweUO0D/1gNlOEaiBZVmmEHeqEVnCORZgcKZwcZyJks/ZBkiYnaU8YKgoc9pzibZgoPdAJrUH80EAImM4vf255R9p3q2x+o5A4DcfH4LH9tY4TPq3gvBg8TAWuAzTZBCJkZp2xsCcvE++ADkcgBGSFB3ZxOaT4yzinKJZQoCKlKxSTaJHV9JRC26zjyO8B8e646jAXHcAsPvXEcbrjccTm4l0BbrQHFNJVVkuyC/WHO+qG0OBnCGe/fAQ1rlSFdU0xrQSjx9sCYCeBP0QVKEie6FqoEttJcY6W7bKBz223Aqha86XDsMFxo0EnhF/q31uuJDdgkBwaEiZSc8cxAIMsaSnluMSoVn3rABKALxTCNrsK0WJVqpoMYC0bntB90EhpdnIVM4GsIDRcF3Rba4yvsdalRfMO9vx/WXFT7pzRjN/IPGJDygBCpEtiwRgZgjCJ402z27oykqDLZBLaSQ5Cc+6CAUkTKSwcBVEkBhADVFCuRSJSEgMROXthUmZyFFnFu4iFrwQTMkqTF+4KttPcODSYdVVNIU7DlvlAwRCiZ/RCAzyRYRs1CAnHGABKsFMQUE03QjBNFHh4QXNJAUGsI55pTApHT9TT0bjLnYRq7oBEwZj453TdS3XafUUO8EQv3wFQBclPeIBBvBMNYT7O2Cl6+F2s/khfXv7lYn2I5OovA4roITdpEf1T5L9Vu/2RLrTXG0D/rgf/cpCBTa2J/yQ4DrjOn3VP88S61qf+Ig9z4Zc/uP9zYGMM7Vtn3rknp8ziN6Q62bN19twLSoaGkUIm6Bo6rqOjUkaL1r1QCQXALbcD2wb3Rf3GzftqmtrDWpBR0D/AOq0UMuV1BCDqrNAByuyMT6myqr5XR/VWkQjyuxhfxdo7CHRPqrVcnUSE/FWiNzs4VvU2i7HzCnAx951NoMVeUOJQ1yhltS7Q0N1GpQIp+Ot+s11x95dLWpRtSSY/pruElbUxPprq1QFpPjCHp76YEBpln5onYv9jU+1BSzfUVGltP4s45Okun9otbvoXRqu9E5vTtm9zX6ngfs6QO+FHUOYcnW3+xpEah1JdsDH7sWwW9BafcuUD7g0sG1Jk90Of1CuL3FzycdRU+2CRZKELMihMl3x5QwNQJmTzc3COq6a7dbbe97XWmOIbqk7VphQQQaJ8DrDuttNuMJa4FwABBQhTKFt9VZcMxcaa8Y/qLUq87ffH9Ra/jb74CXrZ1eVHtmmU4/qLX8bffCC8xVRNQrlX48WQ5W9NbaEyc7mPsiZSU9h/MY1EogKjL6IJnUAHZWAV8yg45pSCQUEwd2cToKIRIO2wbY3EZLjBa9p4UrHKDKgNYDcV+Y74VoA5irs5GWUJ5TQnKS+FIJoFM6r81goQoIJl3JALHuaZojjjuiXU3Vp53e+Gg1QVxMFwC+MHAD5+2BiuGzeN0IE0mQ4GERZElcTSCRVU2DKcMtvcXXOncbZJKnT5mz3FPjLTOkf6d68SrkBIaJSXMmAnWP1BZSpPBEg9Ref6tx557jiSSZVxhTiUXE59ixPyopAKKJQmDVnkAIUKD9Va1lGBcMZ1SM3KpOMu6KqvmwrjBeKIdxkgWKlO/5mHN1AtDQUwJgnDFcJLId8AmpcZEpIgKDsESVVcUOJQjhSAWGeqWEpxQGQQEbICgIKIBGriMVEpQDwG2cFJAKdtMeMEkYLljE5gpq7UlCAKQF4ie+NJGwfMxd1W/VsX0BYChDm0cJHOOexeaFQlGnFMHQzqLLtdq60OY7MH4plpv8A+e2B+85Xe0QD53maJRCkB8pzI4n6DDjMAyTCaNEEGZcUpIrJB2xI1ICURVWWMFDRVd3zhCEx3TTCFPznh2QA76vADd2wVw5ipT51gtUnAEbEh5XyjHYJwBJfrE0We6Gk0UlKKqe0wSuouBcpwIdXvSCcdYl2xWRAI7KQo2YpjCE6aCGmjqjEpElSoiYoi4CMsvGCAqCiQhCYjanvgGiSJKyw7pwgGFVVKe6Pw31+kdp3tfzDvUfFdZfw1kNxUMl4NgFDqSc6ISY0kIAi4ypSEQEgkbJJXiIXVyma40y9sEBBpRDOeyERSSibhOAApWZINRqjLEDfig3QAhmUIrNPbCynNaiW+CRIHDiIvUUAj5pEyDiCVyxjSoABVEnzBJ7oUijdElqTNYf+q5sNRVAE0iZwku8QirOkxj7oBEiZ+yCZAEaUmeaq90AAEjOgwMJWiLko98FShVAhM9sESNFFFovhAnXlnmd0AEKBJcRuwi907nct+2qfrMKjuLvibl00ttLiv6oWHOc5XPcXEmc4IJKFStJTCndB1I1FkN0xPfEhLigUYCDPmMjtQSwgtPlAIlVN8NJkSS0E75+EaQiY1SWMBlACCSkpGFFCE3KkEiRXwiZTPgkdVcPlAn3xb63+4vdas3RrtdPbk5zXTDrjpouQ7YT8KaIvqXFy/Th9z+2XnNuibbN46mEoia01DvjqLF1ht3bLtNxhCFrgZiG4oIAPlmpiQQkrup7oSrgFBoZJE5zkaqDs3RRB7KOSNKySZXMShyColmlV74ckg5UTImExCkHaPmIIAU0OajmoI6S4SjXP0n94en7fiesuKn3Tmg0m/l9sbQNQTEIkO1JQEgnElPZBBT6yz/cmYcteXlxOojLfCuGk6gQabD3QhOzbMJ2BY0uM0JJA4+yO1wCTEvoiZSo2gAiAVSQVopIxnQzmJGcuEHSQhlt8wHGHi10zhZ6o8t9yC3ocfPXLKcMtN8ttoa3c0J8NzrW2j+G6i0xrrrQrfUYSEelCiJDFxbPdKJVJQbZGNQoQK50hqoJeXEIBBQg81BTCEqitnvWNTcUKGnLArIEBd+ffGK07RxgawQtMalEgEUFN/CeMWr1tHOtua4Da10MuN8r2hw3EL8ReAIHqPtsQ4q4FO6FaVqlV5iRP90QCqlyONStECwAvNVxOSbIAcAGgKs/qgrMdvCFJ5moFGYDU8YMtS6tRWiCCQAWkIcEQETgAlCvMlQoUDsgKEUzAzFK7YKCgRqyUlWrwBgOTUChAMhzOPjCuoqHKZkg7o6MCno26fsj8hCFBqDHVsa0NZ6nK1ol5WmghqgooNPKMzACLIEb1AQw0kDUSUbjPfwhQNq0M8UgF0iS2WIUCvZCkzXlOQdKCaKgA2Jh2whFSUSUsfGJKVMgQhliOyJNlWZSaAjshQFcFJBkcHNTsjpbmrU5rBbeR+kzl+I6XpgfM51wrm0Bre3UYDSJT7PKnYYQmRUuGR/NAfMIF05FfdAJGxNhlxglVC4TUSy3QUcqFBlPlJnI0KQrEAQ6VOJK9pnExIFe9BDXNqiyNRMkgb4mFIRQamgQJvhpB1ajnIyke6KagQrF/SBl9lY6Uf9q3gn1Rh+T1I8rSWn/JP3wVnIK4SrMQrQQS1dRmGo4hJxpIllsWOUriFFEovbCgFUUlxWp+mEdzOkQUkCJj58ICKZESQmQEOopqZnu4QrTRE2IC2NQ+tIHcadso1IVCyxkdSS3xfsCtu7q4PaET+H4gWnA6LDGtC0/TLuOoDhCDlBCEGUyDMQiIwALva2f2oAmAgkJzE+2Gh0zqJImPq6t0pxpQgBoKBTpUDl7Ia0lQgFMWzgApIoMgUoNyiCQTmilFIX2xWuIxACA7ISSAAlZTBw7YCVaVaANmUaRTyosjzVMWmgqAxoB3D8l4mQ4MeUw5foWCFnmZoQQKQQpd5iSahQBPhDgJFxRqyKJUxOkzwIp2GJrqVEXEzickWQmmMtkaUCSDcpnhlAJULJxxrWGlygOQt3jmpwMB0q1Cic4rQgSEhUKsdRaUaLlhrjtdbLGf6viPxvRN13US7aCc0kDgtdogh/Q3ySSFFtxCVwGyE/BdQEArbuZAz5Zzjn6S+jiJOt3A6Qr5Y0/hLztQkDauE1xRsIej6hwUOU2ripIEJpgH8H1BqCBZuVQgFdGEEjo+owc5bTxMJjpyE4KdH1BAys3FphywXXOkvhNRAFp6TEvqiCG9HfUuKD0nkgaVly5wv4LqNQC6vSeQqSFIDGdH1BJKK+25qyUTcAEBNTFtr01hoDkooE/yWknzMY7dVso0lMGgCVFxrjBA+sOYezugn9UEuElUqR2JEzI+yUF5dJpV2NJS8IqEKJsIn9EErIhqcDXuhA4KSnMhmoJkN6QXErqCBSMFEo1qCSQZpnqpxgtaw3Ca6Zr2bYvdbftvtMNv0m+o0tc4ktcUBwGlPlTW9bYF3SukqWuC15mEGF/CGRX/AHb1VX/k2xLo25zc8z4ujSOhtESHM3VT9pY1DoLGrM22nxEah0PTg5+kz+WJ9JYKU+7Z7oAHTWgBT7tvugFti20ihDGj2R5G9gjyjsESAG7/AAbf/9k=' + +window.File = function() { + this.name = 'test' +} +window.FileReader = function() { + this.readAsDataURL = this.readAsText = function() { + this.onload && + this.onload({ + target: { + result: _dataUrl, + }, + }) + } +} + +export const dataUrl = _dataUrl diff --git a/components/image-reader/test/index.spec.js b/components/image-reader/test/index.spec.js new file mode 100644 index 00000000..abca1d0b --- /dev/null +++ b/components/image-reader/test/index.spec.js @@ -0,0 +1,54 @@ +import ImageReader from '../index' +import triggerEvent from '../../popup/test/touch-trigger' +import {mount} from 'avoriaz' +import imageProcessor from '../image-processor' +import {dataUrl} from './file.mock' +import Promise from 'es6-promise' + +Promise.polyfill() + +describe('ImageReader', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a image-reader', () => { + wrapper = mount(ImageReader, { + propsData: { + size: 10, + }, + }) + + expect(wrapper.contains('input')).to.equal(true) + + window.Worker = null + wrapper.vm.$_readFile({ + files: [ + { + name: '123.jpg', + lastModified: 1501728385000, + lastModifiedDate: '', + webkitRelativePath: '', + size: 100, + }, + { + name: '123.jpg', + lastModified: 1501728385000, + lastModifiedDate: '', + webkitRelativePath: '', + size: 57070, + }, + ], + }) + }) + it('image-reader processor', () => { + imageProcessor({ + dataUrl, + width: 200, + height: 200, + quality: 0.1, + }) + }) +}) diff --git a/components/image-viewer/README.md b/components/image-viewer/README.md new file mode 100644 index 00000000..b043fd9a --- /dev/null +++ b/components/image-viewer/README.md @@ -0,0 +1,29 @@ +--- +title: ImageViewer 图片查看器 +preview: https://didi.github.io/mand-mobile/examples/image-viewer +--- + +用于浏览多张图片,并可对图片进行滑动切换 + +### 引入 + +```javascript +import { ImageViewer } from 'mand-mobile' + +Vue.component(ImageViewer.name, ImageViewer) +``` + + +### 代码演示 + + +### API + +#### ImageViewer Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +| show | 是否显示查看器 | Boolean | `false`| - | +| list |展示图片列表 | Array | `[]` | -| +| initial-index | 初始索引值 | Number | `0` | - | +| has-dots | 是否展示图片索引值 | Boolean | `true` | - | + diff --git a/components/image-viewer/component.js b/components/image-viewer/component.js new file mode 100644 index 00000000..d277e7c0 --- /dev/null +++ b/components/image-viewer/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'image-viewer', + 'text': '图片查看器', + 'category': 'basic', + 'description': '图片查看器', + 'author': 'linyufei' +} diff --git a/components/image-viewer/demo/cases/demo0.vue b/components/image-viewer/demo/cases/demo0.vue new file mode 100644 index 00000000..cd241be0 --- /dev/null +++ b/components/image-viewer/demo/cases/demo0.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/components/image-viewer/demo/index.vue b/components/image-viewer/demo/index.vue new file mode 100644 index 00000000..9db4a342 --- /dev/null +++ b/components/image-viewer/demo/index.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/components/image-viewer/index.vue b/components/image-viewer/index.vue new file mode 100644 index 00000000..70545506 --- /dev/null +++ b/components/image-viewer/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/components/image-viewer/test/index.spec.js b/components/image-viewer/test/index.spec.js new file mode 100644 index 00000000..8a40314e --- /dev/null +++ b/components/image-viewer/test/index.spec.js @@ -0,0 +1,39 @@ +import ImageViewer from '../index' +import {mount} from 'avoriaz' + +describe('ImageViewer', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple image-viewer', () => { + wrapper = mount(ImageViewer) + + expect(wrapper.hasClass('md-image-viewer')).to.be.true + expect(wrapper.vm.show).to.equal(false) + expect(wrapper.vm.initialIndex).to.equal(0) + expect(wrapper.vm.hasDots).to.equal(true) + }) + + it('imageViewer method afterChange', () => { + wrapper = mount(ImageViewer) + wrapper.vm.$_afterChange(1, 2) + expect(wrapper.vm.currentImgIndex).to.equal(2) + }) + + it('imageViewer method viewerClick', () => { + wrapper = mount(ImageViewer) + wrapper.find('.md-image-viewer')[0].trigger('click') + expect(wrapper.vm.isViewerShow).to.equal(false) + }) + + it('imageViewer method imgsInit', () => { + wrapper = mount(ImageViewer) + wrapper.vm.list = ['aaa.jpg', 'bbb.jpg'] + wrapper.vm.$_imgsInit() + expect(wrapper.vm.imgs[0].url).to.equal('aaa.jpg') + expect(wrapper.vm.imgs[1].url).to.equal('bbb.jpg') + }) +}) diff --git a/components/index.js b/components/index.js new file mode 100644 index 00000000..0c35efac --- /dev/null +++ b/components/index.js @@ -0,0 +1,154 @@ +/* eslint comma-dangle: ["error", "always-multiline"] */ + +// 组件引入 +import './_style/global.styl' +import {warn} from './_util' +import Button from './button' +import Icon from './icon' +import Popup from './popup' +import PopupTitleBar from './popup-title-bar' +import ActionBar from './action-bar' +import ActionSheet from './action-sheet' +import DropMenu from './drop-menu' +import TabBar from './tab-bar' +import Picker from './picker' +import Selector from './selector' +import Swiper from './swiper' +import SwiperItem from './swiper-item' +import Toast from './toast' +import Tip from './tip' +import Tabs from './tabs' +import Tag from './tag' +import InputItem from './input-item' +import Stepper from './stepper' +import Steps from './steps' +import NoticeBar from './notice-bar' +import ImageReader from './image-reader' +import ImageViewer from './image-viewer' +import NumberKeyboard from './number-keyboard' +import Landscape from './landscape' +import ResultPage from './result-page' +import TabPicker from './tab-picker' +import Dialog from './dialog' +import Field from './field' +import FieldItem from './field-item' +import Switch from './switch' +import Agree from './agree' +import Radio from './radio' +import DatePicker from './date-picker' +import Captcha from './captcha' +import Codebox from './codebox' +import Cashier from './cashier' +import Chart from './chart' +/* @init<%import ${componentNameUpper} from './${componentName}'%> */ + +// 全量引入提醒 +warn( + 'You are using a whole package of mand-mobile, ' + + 'please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.', + 'warn', +) + +/* global MAN_VERSION */ +const version = /* @echo MAN_VERSION */ MAN_VERSION + +// 单个组件暴露 +export const components = { + Button, + Icon, + Popup, + PopupTitleBar, + ActionBar, + ActionSheet, + DropMenu, + Picker, + Selector, + TabBar, + Swiper, + SwiperItem, + Tip, + Tabs, + Tag, + InputItem, + Stepper, + Steps, + NoticeBar, + ImageReader, + ImageViewer, + NumberKeyboard, + Landscape, + ResultPage, + TabPicker, + Dialog, + Field, + FieldItem, + Switch, + Agree, + Radio, + DatePicker, + Captcha, + Codebox, + Cashier, + Chart, + /* @init<%${componentNameUpper},%> */ +} + +// 定义插件安装方法 +const install = function(Vue) { + if (!Vue || install.installed) { + return + } + + components.forEach(component => { + component.name && Vue.component(component.name, component) + }) +} + +// 集合组件暴露 +export { + install, + version, + Button, + Icon, + Popup, + PopupTitleBar, + ActionBar, + ActionSheet, + DropMenu, + Picker, + Selector, + TabBar, + Swiper, + SwiperItem, + Toast, + Tip, + Tabs, + Tag, + InputItem, + Stepper, + Steps, + NoticeBar, + ImageReader, + ImageViewer, + NumberKeyboard, + Landscape, + ResultPage, + TabPicker, + Dialog, + Field, + FieldItem, + Switch, + Agree, + Radio, + DatePicker, + Captcha, + Codebox, + Cashier, + Chart, + /* @init<%${componentNameUpper},%> */ +} + +export default { + install, + version, +} diff --git a/components/input-item/README.md b/components/input-item/README.md new file mode 100644 index 00000000..29e4236c --- /dev/null +++ b/components/input-item/README.md @@ -0,0 +1,81 @@ +--- +title: InputItem 输入框 +preview: https://didi.github.io/mand-mobile/examples/input-item +--- + +单行文本输入框,支持特殊场景文本格式化 + +### 引入 + +```javascript +import { InputItem } from 'mand-mobile' + +Vue.component(InputItem.name, InputItem) +``` + +### 代码演示 + + +### API + +#### InputItem Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|type|表单类型,特殊类型自带文本格式化|String|`text`|`text`,`bankCard`,`phone`,
`money`,`password`| +|name|表单名称|String|-|事件入参之一,可用于区分表单组件| +|v-model|表单值|String|-|-| +|title|表单左侧标题|String|-|可以传入`HtmlFragment`,也可直接使用`slot left`代替| +|placeholder|表单占位符|String|-|-| +|maxlength|表单最大字符数|String/Number|-|`phone`类型固定为11| +|size|表单尺寸|String|`normal`|`large`,`normal`| +|align|表单文本对齐方式|String|`left`|`left`,`center`,`right`| +|error|表单错误提示信息|String|-|-| +|readonly|表单是否只读|Boolean|`false`|-| +|disabled|表单是否禁用|Boolean|`false`|-| +|is-title-latent|表单标题是否隐藏|Boolean|`false`|表单获得焦点或内容不为空时展示| +|is-highlight|表单是否高亮|Boolean|`false`|只影响`placeholder`字体颜色| +|is-formative|表单文本是否根据类型自动格式化|Boolean|`type`为`bankCard`,`phone`, `money`默认为`true`,否则为`false`|-| +|formation|表单文本格式化回调方法|Function(name, curValue, curPos): {value: curValue, range: curPos}|-|传入参数`name`为表单名称,`curValue`为表单值,`curPos`为表单光标当前所在位置
返回参数`value`格式化值, `range`表单光标格式化后所在位置| +|clearable|表单是否使用清除控件|Boolean|`false`|-| +|is-virtual-keyboard|表单是否使用金融数字键盘控件|Boolean|`false`|-| +|virtual-keyboard-disorder|金融数字键盘数字键乱序|Boolean|`false`|-| +|virtual-keyboard-ok-text|金融数字键盘确认键文案|String|`确定`|-| + +#### InputItem Slots + +#### left +左侧插槽,一般用于放置图标等 + +#### right +右侧插槽,一般用于放置图标等 + +#### InputItem Methods + +##### focus() +表单获得焦点 + +##### blur() +表单失去焦点 + +##### getValue() +获取表单值 + +#### InputItem Events + +##### @focus(name) +表单获得焦点事件 + +##### @blur(name) +表单失去焦点事件 + +##### @change(name, value) +表单值变化事件 + +##### @confirm(name, value) +表单值确认事件, 仅使用金融数字键盘或组件在`form`元素内时有效 + +##### @keyup(name, event) +表单按键按下事件,仅`is-virtual-keyboard`为`false`时触发 + +##### @keydown(name, event) +表单按键释放事件,仅`is-virtual-keyboard`为`false`时触发 diff --git a/components/input-item/component.js b/components/input-item/component.js new file mode 100644 index 00000000..27d62bf4 --- /dev/null +++ b/components/input-item/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'input-item', + 'text': '输入框', + 'category': 'form', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/input-item/cursor.js b/components/input-item/cursor.js new file mode 100644 index 00000000..24ee3a2f --- /dev/null +++ b/components/input-item/cursor.js @@ -0,0 +1,44 @@ +/** + * get position of input cursor + */ +export function getCursorsPosition(ctrl) { + /* istanbul ignore if */ + if (!ctrl) { + return 0 + } + let CaretPos = 0 // IE Support + /* istanbul ignore next */ + if (document.selection) { + ctrl.focus() + const Sel = document.selection.createRange() + Sel.moveStart('character', -ctrl.value.length) + CaretPos = Sel.text.length + } else if (ctrl.selectionStart || ctrl.selectionStart === '0') { + // Firefox support + CaretPos = ctrl.selectionStart + } + return CaretPos +} + +/** + * set position of input cursor + */ +export function setCursorsPosition(ctrl, pos) { + /* istanbul ignore if */ + if (!ctrl) { + return + } + setTimeout(() => { + /* istanbul ignore next */ + if (ctrl.setSelectionRange) { + ctrl.focus() + ctrl.setSelectionRange(pos, pos) + } else if (ctrl.createTextRange) { + const range = ctrl.createTextRange() + range.collapse(true) + range.moveEnd('character', pos) + range.moveStart('character', pos) + range.select() + } + }, 0) +} diff --git a/components/input-item/demo/cases/demo0.vue b/components/input-item/demo/cases/demo0.vue new file mode 100644 index 00000000..ec41c24f --- /dev/null +++ b/components/input-item/demo/cases/demo0.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/cases/demo1.vue b/components/input-item/demo/cases/demo1.vue new file mode 100644 index 00000000..ff05e728 --- /dev/null +++ b/components/input-item/demo/cases/demo1.vue @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/cases/demo2.vue b/components/input-item/demo/cases/demo2.vue new file mode 100644 index 00000000..8c2899fc --- /dev/null +++ b/components/input-item/demo/cases/demo2.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/cases/demo3.vue b/components/input-item/demo/cases/demo3.vue new file mode 100644 index 00000000..ee51ca2c --- /dev/null +++ b/components/input-item/demo/cases/demo3.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/components/input-item/demo/cases/demo4.vue b/components/input-item/demo/cases/demo4.vue new file mode 100644 index 00000000..c27ce068 --- /dev/null +++ b/components/input-item/demo/cases/demo4.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/index.vue b/components/input-item/demo/index.vue new file mode 100644 index 00000000..02488a7a --- /dev/null +++ b/components/input-item/demo/index.vue @@ -0,0 +1,20 @@ + + + diff --git a/components/input-item/index.vue b/components/input-item/index.vue new file mode 100644 index 00000000..f356d6d0 --- /dev/null +++ b/components/input-item/index.vue @@ -0,0 +1,657 @@ + + + + + diff --git a/components/input-item/keycode.js b/components/input-item/keycode.js new file mode 100644 index 00000000..8a78ddf1 --- /dev/null +++ b/components/input-item/keycode.js @@ -0,0 +1,32 @@ +// http://www.t086.com/article/4315 +export const keyCodeList = { + bankCard: ['8', '13', '48-57', '96-105', '108', '229'], + phone: ['8', '13', '48-57', '96-105', '108', '229'], + money: ['8', '13', '48-57', '96-105', '108', '110', '190', '229'], +} + +export function isValidKey(type, code) { + const list = keyCodeList[type] || '' + + if (!list) { + return true + } + + let res = false + + for (let i = 0, len = list.length; i < len; i++) { + const itemParts = list[i].split('-') + const min = +itemParts[0] + const max = +itemParts[1] || null + + if (max === null && code === min) { + res = true + break + } else if (max !== null && code >= min && code <= max) { + res = true + break + } + } + + return res +} diff --git a/components/input-item/test/index.spec.js b/components/input-item/test/index.spec.js new file mode 100644 index 00000000..aea76152 --- /dev/null +++ b/components/input-item/test/index.spec.js @@ -0,0 +1,139 @@ +import InputItem from '../index' +import triggerEvent from '../../popup/test/touch-trigger' +import {mount} from 'avoriaz' + +describe('InputItem', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a input-item', () => { + wrapper = mount(InputItem, { + propsData: { + value: 'test', + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const input = wrapper.find('.md-input-item-input')[0] + wrapper.vm.inputValue = '123' + // expect(eventStub.calledWith('change')).to.be.true + + input.trigger('focus') + expect(eventStub.calledWith('focus')).to.be.true + input.trigger('blur') + expect(eventStub.calledWith('blur')).to.be.true + + wrapper.vm.focus() + triggerEvent(input.element, 'keydown', 0, 0, 49) + triggerEvent(input.element, 'keyup', 0, 0, 49) + + wrapper.vm.blur() + expect(wrapper.vm.getValue()).to.equal('123') + + wrapper.setProps({value: '456'}) + expect(wrapper.vm.getValue()).to.equal('456') + }) + + it('input-item keyup/down', () => { + wrapper = mount(InputItem, { + propsData: { + type: 'bankCard', + value: '123123', + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const input = wrapper.find('.md-input-item-input')[0] + + wrapper.vm.focus() + triggerEvent(input.element, 'keydown', 0, 0, 49) + triggerEvent(input.element, 'keyup', 0, 0, 49) + + triggerEvent(input.element, 'keydown', 0, 0, 11) + triggerEvent(input.element, 'keyup', 0, 0, 11) + + triggerEvent(input.element, 'keydown', 0, 0, 13) + triggerEvent(input.element, 'keyup', 0, 0, 13) + expect(eventStub.calledWith('confirm')).to.be.true + // wrapper.vm.blur() + }) + + it('phone input-item', () => { + wrapper = mount(InputItem, { + propsData: { + type: 'phone', + value: '123123123123123123', + }, + }) + + expect(wrapper.vm.inputValue.length).to.equal(13) + }) + + it('input-item with clear btn', () => { + wrapper = mount(InputItem, { + propsData: { + value: 'test', + clearable: true, + }, + }) + expect(wrapper.vm.isInputEmpty).to.be.false + const clearBtn = wrapper.find('.md-input-item-clear')[0] + clearBtn.trigger('click') + expect(wrapper.vm.isInputEmpty).to.be.true + }) + + it('input-item with number-keyborad', done => { + wrapper = mount(InputItem, { + propsData: { + type: 'money', + isVirtualKeyboard: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const input = wrapper.find('.md-input-item-fake')[0] + + // input.trigger('click') + + wrapper.vm.focus() + setTimeout(() => { + const keyborad = wrapper.find('.md-number-keyboard')[0] + keyborad.find('.delete')[0].trigger('click') + keyborad.find('.keyboard-number-item')[1].trigger('click') + keyborad.find('.keyboard-number-item')[9].trigger('click') + keyborad.find('.keyboard-number-item')[9].trigger('click') + keyborad.find('.keyboard-number-item')[2].trigger('click') + keyborad.find('.keyboard-number-item')[2].trigger('click') + keyborad.find('.delete')[0].trigger('click') + expect(wrapper.vm.inputValue).to.equal('2.3') + keyborad.find('.confirm')[0].trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + wrapper.vm.blur() + done() + }, 500) + // wrapper.vm.blur() + }) + + it('input-item disabled', () => { + wrapper = mount(InputItem, { + propsData: { + disabled: true, + isVirtualKeyboard: true, + value: '123', + formation(name, curValue, curPos) { + return { + value: curValue, + range: curPos, + } + }, + }, + }) + + const input = wrapper.find('.md-input-item-fake')[0] + + input.trigger('click') + }) +}) diff --git a/components/landscape/README.md b/components/landscape/README.md new file mode 100644 index 00000000..60f4c4ba --- /dev/null +++ b/components/landscape/README.md @@ -0,0 +1,34 @@ +--- +title: Landscape 输入框 +preview: https://didi.github.io/mand-mobile/examples/landscape +--- + +用于在浮层中显示广告或说明 + +### 引入 + +```javascript +import { Landscape } from 'mand-landscape' + +Vue.component(Landscape.name, Landscape) +``` + +### 代码演示 + + +### API + +#### Landscape Props +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|v-model|是否展示|Boolean|`false`| +|has-mask|是否有蒙层|Boolean|`true`| +|scroll|内容区域是否可以滚动|Boolean|`false`| + +#### Landscape Events + +##### @show() +弹出层展示事件 + +##### @hide() +弹出层隐藏事件 diff --git a/components/landscape/component.js b/components/landscape/component.js new file mode 100644 index 00000000..e542f64a --- /dev/null +++ b/components/landscape/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'landscape', + 'text': '压屏', + 'category': 'business', + 'description': '用于展示压屏广告或通知的组件。', + 'author': 'zhaozhe' +} diff --git a/components/landscape/demo/cases/demo0.vue b/components/landscape/demo/cases/demo0.vue new file mode 100644 index 00000000..92d4acb9 --- /dev/null +++ b/components/landscape/demo/cases/demo0.vue @@ -0,0 +1,67 @@ + + + + + \ No newline at end of file diff --git a/components/landscape/demo/index.vue b/components/landscape/demo/index.vue new file mode 100644 index 00000000..b038da96 --- /dev/null +++ b/components/landscape/demo/index.vue @@ -0,0 +1,17 @@ + + + diff --git a/components/landscape/index.vue b/components/landscape/index.vue new file mode 100644 index 00000000..943dbbbb --- /dev/null +++ b/components/landscape/index.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/components/landscape/test/index.spec.js b/components/landscape/test/index.spec.js new file mode 100644 index 00000000..17db5b12 --- /dev/null +++ b/components/landscape/test/index.spec.js @@ -0,0 +1,49 @@ +import Landscape from '../index' +import {mount} from 'avoriaz' + +describe('Landscape', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple landscape', () => { + wrapper = mount(Landscape) + + expect(wrapper.hasClass('md-landscape')).to.be.true + }) + + it('create a simple landscape and open it', done => { + wrapper = mount(Landscape, { + propsData: { + value: false, + }, + }) + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('none') + + wrapper.vm.value = true + + setTimeout(() => { + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('') + done() + }, 300) + }) + + it('create a simple landscape and close it', done => { + wrapper = mount(Landscape, { + propsData: { + value: true, + }, + }) + setTimeout(() => { + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('') + wrapper.find('.close')[0].trigger('click') + }, 300) + + setTimeout(() => { + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('none') + done() + }, 600) + }) +}) diff --git a/components/notice-bar/README.md b/components/notice-bar/README.md new file mode 100644 index 00000000..485312b8 --- /dev/null +++ b/components/notice-bar/README.md @@ -0,0 +1,28 @@ +--- +title: Notice 通告栏 +preview: https://didi.github.io/mand-mobile/examples/notice-bar +--- + +通常用于系统提醒、活动提醒等通知 + +### 引入 + +```javascript +import { NoticeBar } from 'mand-mobile' + +Vue.component(NoticeBar.name, NoticeBar) +``` + + +### 代码演示 + + +### API + +#### NoticeBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|closable|是否可关闭|Boolean|`true`|-| +|time|显示时长|Number|`0`|单位为`ms`,不需要自动消失可将其置为`0`| +|icon|在开始位置的图标样式|String|`circle-alert`|-| + diff --git a/components/notice-bar/component.js b/components/notice-bar/component.js new file mode 100644 index 00000000..51cdd480 --- /dev/null +++ b/components/notice-bar/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'notice-bar', + 'text': '通告栏', + 'category': 'basic', + 'description': '通告栏', + 'author': 'linyufei' +} diff --git a/components/notice-bar/demo/cases/demo0.vue b/components/notice-bar/demo/cases/demo0.vue new file mode 100644 index 00000000..55172e41 --- /dev/null +++ b/components/notice-bar/demo/cases/demo0.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/components/notice-bar/demo/cases/demo1.vue b/components/notice-bar/demo/cases/demo1.vue new file mode 100644 index 00000000..4e811580 --- /dev/null +++ b/components/notice-bar/demo/cases/demo1.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/notice-bar/demo/cases/demo2.vue b/components/notice-bar/demo/cases/demo2.vue new file mode 100644 index 00000000..7ad1cdfe --- /dev/null +++ b/components/notice-bar/demo/cases/demo2.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/components/notice-bar/demo/index.vue b/components/notice-bar/demo/index.vue new file mode 100644 index 00000000..6ed5e056 --- /dev/null +++ b/components/notice-bar/demo/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/notice-bar/index.vue b/components/notice-bar/index.vue new file mode 100644 index 00000000..8df72c3a --- /dev/null +++ b/components/notice-bar/index.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/components/notice-bar/test/index.spec.js b/components/notice-bar/test/index.spec.js new file mode 100644 index 00000000..f379a56b --- /dev/null +++ b/components/notice-bar/test/index.spec.js @@ -0,0 +1,46 @@ +import NoticeBar from '../index' +import {mount} from 'avoriaz' + +describe('NoticeBar', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple notice-bar', () => { + wrapper = mount(NoticeBar) + + expect(wrapper.hasClass('md-notice-bar')).to.be.true + expect(wrapper.vm.closable).to.equal(true) + expect(wrapper.vm.time).to.equal(0) + expect(wrapper.vm.icon).to.equal('circle-alert') + }) + + it('mount time is not null', done => { + wrapper = mount(NoticeBar, { + propsData: { + time: 500, + }, + }) + setTimeout(() => { + expect(wrapper.vm.isShow).to.equal(false) + done() + }, 1000) + }) + + it('notice-bar method hide', done => { + wrapper = mount(NoticeBar) + wrapper.vm.$_hide(500) + setTimeout(() => { + expect(wrapper.vm.isShow).to.equal(false) + done() + }, 1000) + }) + + it('notice-bar method close', () => { + wrapper = mount(NoticeBar) + wrapper.vm.$_close() + expect(wrapper.vm.isShow).to.equal(false) + }) +}) diff --git a/components/number-keyboard/README.md b/components/number-keyboard/README.md new file mode 100644 index 00000000..a3027014 --- /dev/null +++ b/components/number-keyboard/README.md @@ -0,0 +1,51 @@ +--- +title: NumberKeyboard 数字键盘 +preview: https://didi.github.io/mand-mobile/examples/number-keyboard +--- + +一般用于密码,验证码或支付金额输入等金融场景 + +### 引入 + +```javascript +import { NumberKeyboard } from 'mand-mobile' + +Vue.component(NumberKeyboard.name, NumberKeyboard) +``` + +### 代码演示 + + +### API + +#### NumberKeyboard Props +|属性 | 说明 | 类型 | 默认值| 备注| +|----|-----|------|------|------| +|v-model|键盘是否展示|Boolean|`false`|-| +|is-view|是否内嵌在页面内展示,否则以弹层形式|Boolean|`false`|-| +|type|键盘类型|String|`professional`|`professional`有确认键和小数点常用于价格或金额输入,`simple`一般用于密码或验证码输入| +|disorder|键盘数字键是否乱序|Boolean|`false`| -| +|ok-text|键盘确认键文案|String|`确认`|-| + +#### NumberKeyboard Methods + +##### show() +展示键盘 + +##### hide() +隐藏键盘 + +#### NumberKeyboard Events + +##### @enter(val) +数字键点击事件 + +属性 | 说明 | 类型 +----|-----|------ +val | 数字 | Number + +##### @delete() +回退键点击事件 + +##### @confirm() +确认键点击事件 diff --git a/components/number-keyboard/component.js b/components/number-keyboard/component.js new file mode 100644 index 00000000..3e53fcc3 --- /dev/null +++ b/components/number-keyboard/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'number-keyboard', + 'text': '数字键盘', + 'category': 'form', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/number-keyboard/demo/cases/demo0.vue b/components/number-keyboard/demo/cases/demo0.vue new file mode 100644 index 00000000..4ef28e1d --- /dev/null +++ b/components/number-keyboard/demo/cases/demo0.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/components/number-keyboard/demo/cases/demo1.vue b/components/number-keyboard/demo/cases/demo1.vue new file mode 100644 index 00000000..40fb300f --- /dev/null +++ b/components/number-keyboard/demo/cases/demo1.vue @@ -0,0 +1,54 @@ + + + + + \ No newline at end of file diff --git a/components/number-keyboard/demo/cases/demo2.vue b/components/number-keyboard/demo/cases/demo2.vue new file mode 100644 index 00000000..705ccf37 --- /dev/null +++ b/components/number-keyboard/demo/cases/demo2.vue @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/components/number-keyboard/demo/index.vue b/components/number-keyboard/demo/index.vue new file mode 100644 index 00000000..ea3be7ab --- /dev/null +++ b/components/number-keyboard/demo/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/components/number-keyboard/index.vue b/components/number-keyboard/index.vue new file mode 100644 index 00000000..0330123b --- /dev/null +++ b/components/number-keyboard/index.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/components/number-keyboard/keyboard.vue b/components/number-keyboard/keyboard.vue new file mode 100644 index 00000000..aa0b0ee6 --- /dev/null +++ b/components/number-keyboard/keyboard.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/components/number-keyboard/test/index.spec.js b/components/number-keyboard/test/index.spec.js new file mode 100644 index 00000000..75b86113 --- /dev/null +++ b/components/number-keyboard/test/index.spec.js @@ -0,0 +1,56 @@ +import NumberKeyboard from '../index' +import {mount} from 'avoriaz' + +describe('NumberKeyboard', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a number-keyboard', done => { + wrapper = mount(NumberKeyboard) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const numberBtn = wrapper.find('.keyboard-number-item')[1] + + wrapper.vm.disorder = true + wrapper.vm.value = true + setTimeout(() => { + numberBtn.trigger('click') + expect(eventStub.calledWith('enter')).to.be.true + + wrapper.vm.isKeyboardShow = false + expect(eventStub.calledWith('input')).to.be.true + done() + }, 300) + }) + + it('number-keyboard delete', done => { + wrapper = mount(NumberKeyboard) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const deleteBtn = wrapper.find('.keyboard-operate-item')[0] + + wrapper.vm.value = true + setTimeout(() => { + deleteBtn.trigger('click') + expect(eventStub.calledWith('delete')).to.be.true + done() + }, 300) + }) + + it('number-keyboard confirm', done => { + wrapper = mount(NumberKeyboard) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const confirmBtn = wrapper.find('.keyboard-operate-item')[1] + + wrapper.vm.value = true + setTimeout(() => { + confirmBtn.trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }, 300) + }) +}) diff --git a/components/picker/README.md b/components/picker/README.md new file mode 100644 index 00000000..455ce35a --- /dev/null +++ b/components/picker/README.md @@ -0,0 +1,172 @@ +--- +title: Picker 选择器 +preview: https://didi.github.io/mand-mobile/examples/picker +--- + +滚动多列选择 + +### 引入 + +```javascript +import { Picker } from 'mand-mobile' + +Vue.component(Picker.name, Picker) +``` + +### 代码演示 + + +### API + +#### Picker Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|选择器是否可见|Boolean|`false`|-| +|data|数据源|Array<{value, lable, ...}>[]|`[]`|-| +|cols|数据列数|Number|`1`|-| +|default-index|选择器各列初始选中项索引|Array|`[]`|-| +|invalid-index|选择器各列不可用选项索引|Array|`[]`|某列多个不可用项使用数组,单个使用数字, 如`[[1,2], 2]`| +|is-view|是否内嵌在页面内展示,否则以弹层形式|Boolean|`false`|-| +|is-cascade|各列数据是否级联|Boolean|`false`|级联数据格式见附录| +|title|选择器标题|String|-|-| +|ok-text|选择器确认文案|String|`确认`|-| +|cancel-text|选择器取消文案|String|`取消`|-| + +#### Picker Methods + +##### refresh(callback, startColumnIndex) +重新初始化选择器,如更新`data`, `default-index`或`invalid-index` + +|参数 | 说明 | 类型| +|----|-----|------| +|callback|初始化完成回调|Function| +|startColumnIndex|从某列开始重置,默认为0|Function| + +##### getColumnValue(index): activeItemValue +获取某列当前选中项的值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|activeItemValue|选中项的值|Object: {value, lable, ...}| + +##### getColumnValues(): columnsValue +获取所有列选中项的值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{value, lable, ...}>| + +##### getColumnIndex(index): activeItemIndex +获取某列当前选中项的索引值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|activeItemIndex|选中项的索引值|Number| + +##### getColumnIndexs(): columnsIndex +获取所有列选中项的索引值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|columnsIndex|所有列选中项的索引值|Array| + +##### setColumnValues(index, values, callback) +设置某列数据 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|列索引|Number| +|values|列数据|Array<{value, lable, ...}>| +|callback|列数据设置完成回调|Function| + +#### Picker Events + +##### @initialed() +选择器数据初始化完成事件,可调用`getColumnIndex`, `getColumnIndexs`, `getColumnValue`, `getColumnValues`方法 + +##### @change(columnIndex, itemIndex, value) +选择器选中项更改事件 + +|参数 | 说明 | 类型| +|----|-----|------| +|columnIndex|更改列的索引值|Number| +|itemIndex|更改列选中项的索引值|Number| +|value|更改列选中项的的值|Object: {value, lable, ...}| + +##### @confirm(columnsValue) +选择器确认选择事件(仅`is-view`为`false`) + +|参数 | 说明 | 类型| +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{value, lable, ...}>| + +##### @cancel() +选择器取消选择事件(仅`is-view`为`false`) + +##### @show() +选择器弹层展示事件(仅`is-view`为`false`) + +##### @hide() +选择器弹层隐藏事件(仅`is-view`为`false`) + +### 附录 + +* 非级联数据源数据格式 + +```javascript +[ + [ + { + // 选项展示文案 + "text": "", + // 以下自定义字段 + "value": "" + }, + // ... + ], + // ... +] +``` + +* 级联数据源数据格式 + +```javascript +[ + [ + { + // 选项展示文案 + "text": "", + // 第二列对应数据 + "children": [ + { + "text": "", + // 第三列对应数据 + "children": [ + // ... + ] + }, + // ... + ] + // 以下自定义字段 + "value": "" + }, + // ... + ] +] +``` diff --git a/components/picker/cascade.js b/components/picker/cascade.js new file mode 100644 index 00000000..b226ffdb --- /dev/null +++ b/components/picker/cascade.js @@ -0,0 +1,37 @@ +import {warn} from '../_util' + +const defaultOptions = { + currentLevel: 0, + maxLevel: 0, + values: [], + defaultIndex: [], +} + +/** + * cascade column by set value of following columns + * @param {*} picker instance of picker-column + * @param {*} options { currentLevel, maxLevel, values } + * @param {*} fn + */ +export default function(picker, options = {}, fn) { + // options = {...defaultOptions, ...options} + options = Object.assign({}, defaultOptions, options) + + /* istanbul ignore if */ + if (!picker) { + warn('cascade: picker is undefined') + return + } + + let values = options.values + + /* istanbul ignore next */ + for (let i = options.currentLevel + 1; i < options.maxLevel; i++) { + const activeIndex = options.defaultIndex[i] || 0 + const columnValues = values.children || [] + picker.setColumnValues(i, columnValues) + values = columnValues[activeIndex] || [] + } + + fn && fn() +} diff --git a/components/picker/component.js b/components/picker/component.js new file mode 100644 index 00000000..09e997a5 --- /dev/null +++ b/components/picker/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'picker', + 'text': '选择器', + 'category': 'feedback', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/picker/demo/cases/demo0.vue b/components/picker/demo/cases/demo0.vue new file mode 100644 index 00000000..f424d954 --- /dev/null +++ b/components/picker/demo/cases/demo0.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/components/picker/demo/cases/demo1.vue b/components/picker/demo/cases/demo1.vue new file mode 100644 index 00000000..5b49dcae --- /dev/null +++ b/components/picker/demo/cases/demo1.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/components/picker/demo/cases/demo2.vue b/components/picker/demo/cases/demo2.vue new file mode 100644 index 00000000..d135e541 --- /dev/null +++ b/components/picker/demo/cases/demo2.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/components/picker/demo/data/district.js b/components/picker/demo/data/district.js new file mode 100644 index 00000000..0b34f06e --- /dev/null +++ b/components/picker/demo/data/district.js @@ -0,0 +1,1261 @@ +export default [[{ + "value": "340000", + "label": "安徽省", + "children": [{ + "value": "341500", + "label": "六安市", + "children": [{ + "value": "341522", + "label": "霍邱县", + "children": [] + }, { + "value": "341502", + "label": "金安区", + "children": [] + }, { + "value": "341524", + "label": "金寨县", + "children": [] + }, { + "value": "341526", + "label": "其它区", + "children": [] + }, { + "value": "341521", + "label": "寿县", + "children": [] + }, { + "value": "341523", + "label": "舒城县", + "children": [] + }, { + "value": "341503", + "label": "裕安区", + "children": [] + }] + }, { + "value": "340500", + "label": "马鞍山市", + "children": [{ + "value": "340506", + "label": "博望区", + "children": [] + }] + }, { + "value": "341800", + "label": "宣城市", + "children": [{ + "value": "341822", + "label": "广德县", + "children": [] + }, { + "value": "341824", + "label": "绩溪县", + "children": [] + }, { + "value": "341825", + "label": "旌德县", + "children": [] + }] + }] +}, { + "value": "820000", + "label": "澳门特别行政区", + "children": [{ + "value": "820100", + "label": "澳门半岛", + "children": [] + }, { + "value": "820200", + "label": "离岛", + "children": [] + }] +}, { + "value": "110000", + "label": "北京", + "children": [{ + "value": "110100", + "label": "北京市", + "children": [{ + "value": "110114", + "label": "昌平区", + "children": [] + }, { + "value": "110105", + "label": "朝阳区", + "children": [] + }, { + "value": "110103", + "label": "崇文区", + "children": [] + }, { + "value": "110115", + "label": "大兴区", + "children": [] + }, { + "value": "110101", + "label": "东城区", + "children": [] + }, { + "value": "110111", + "label": "房山区", + "children": [] + }, { + "value": "110106", + "label": "丰台区", + "children": [] + }, { + "value": "110108", + "label": "海淀区", + "children": [] + }, { + "value": "110116", + "label": "怀柔区", + "children": [] + }, { + "value": "110109", + "label": "门头沟区", + "children": [] + }, { + "value": "110228", + "label": "密云县", + "children": [] + }, { + "value": "110117", + "label": "平谷区", + "children": [] + }, { + "value": "110230", + "label": "其它区", + "children": [] + }, { + "value": "110107", + "label": "石景山区", + "children": [] + }, { + "value": "110113", + "label": "顺义区", + "children": [] + }, { + "value": "110112", + "label": "通州区", + "children": [] + }, { + "value": "110102", + "label": "西城区", + "children": [] + }, { + "value": "110104", + "label": "宣武区", + "children": [] + }, { + "value": "110229", + "label": "延庆县", + "children": [] + }] + }] +}, { + "value": "450000", + "label": "广西壮族自治区", + "children": [{ + "value": "450500", + "label": "北海市", + "children": [{ + "value": "450502", + "label": "海城区", + "children": [] + }, { + "value": "450521", + "label": "合浦县", + "children": [] + }, { + "value": "450522", + "label": "其它区", + "children": [] + }, { + "value": "450512", + "label": "铁山港区", + "children": [] + }, { + "value": "450503", + "label": "银海区", + "children": [] + }] + }, { + "value": "451000", + "label": "百色市", + "children": [{ + "value": "451024", + "label": "德保县", + "children": [] + }, { + "value": "451025", + "label": "靖西县", + "children": [] + }, { + "value": "451028", + "label": "乐业县", + "children": [] + }, { + "value": "451027", + "label": "凌云县", + "children": [] + }, { + "value": "451031", + "label": "隆林各族自治县", + "children": [] + }, { + "value": "451026", + "label": "那坡县", + "children": [] + }, { + "value": "451023", + "label": "平果县", + "children": [] + }, { + "value": "451032", + "label": "其它区", + "children": [] + }, { + "value": "451022", + "label": "田东县", + "children": [] + }, { + "value": "451029", + "label": "田林县", + "children": [] + }, { + "value": "451021", + "label": "田阳县", + "children": [] + }, { + "value": "451030", + "label": "西林县", + "children": [] + }, { + "value": "451002", + "label": "右江区", + "children": [] + }] + }, { + "value": "451400", + "label": "崇左市", + "children": [{ + "value": "451424", + "label": "大新县", + "children": [] + }, { + "value": "451421", + "label": "扶绥县", + "children": [] + }, { + "value": "451402", + "label": "江州区", + "children": [] + }, { + "value": "451423", + "label": "龙州县", + "children": [] + }, { + "value": "451422", + "label": "宁明县", + "children": [] + }, { + "value": "451481", + "label": "凭祥市", + "children": [] + }, { + "value": "451482", + "label": "其它区", + "children": [] + }, { + "value": "451425", + "label": "天等县", + "children": [] + }] + }, { + "value": "450600", + "label": "防城港市", + "children": [{ + "value": "450681", + "label": "东兴市", + "children": [] + }, { + "value": "450603", + "label": "防城区", + "children": [] + }, { + "value": "450602", + "label": "港口区", + "children": [] + }, { + "value": "450682", + "label": "其它区", + "children": [] + }, { + "value": "450621", + "label": "上思县", + "children": [] + }] + }, { + "value": "450800", + "label": "贵港市", + "children": [{ + "value": "450802", + "label": "港北区", + "children": [] + }, { + "value": "450803", + "label": "港南区", + "children": [] + }, { + "value": "450881", + "label": "桂平市", + "children": [] + }, { + "value": "450821", + "label": "平南县", + "children": [] + }, { + "value": "450882", + "label": "其它区", + "children": [] + }, { + "value": "450804", + "label": "覃塘区", + "children": [] + }] + }, { + "value": "450300", + "label": "桂林市", + "children": [{ + "value": "450303", + "label": "叠彩区", + "children": [] + }, { + "value": "450332", + "label": "恭城瑶族自治县", + "children": [] + }, { + "value": "450327", + "label": "灌阳县", + "children": [] + }, { + "value": "450331", + "label": "荔浦县", + "children": [] + }, { + "value": "450322", + "label": "临桂区", + "children": [] + }, { + "value": "450323", + "label": "灵川县", + "children": [] + }, { + "value": "450328", + "label": "龙胜各族自治县", + "children": [] + }, { + "value": "450330", + "label": "平乐县", + "children": [] + }, { + "value": "450333", + "label": "其它区", + "children": [] + }, { + "value": "450305", + "label": "七星区", + "children": [] + }, { + "value": "450324", + "label": "全州县", + "children": [] + }, { + "value": "450304", + "label": "象山区", + "children": [] + }, { + "value": "450325", + "label": "兴安县", + "children": [] + }, { + "value": "450302", + "label": "秀峰区", + "children": [] + }, { + "value": "450311", + "label": "雁山区", + "children": [] + }, { + "value": "450321", + "label": "阳朔县", + "children": [] + }, { + "value": "450326", + "label": "永福县", + "children": [] + }, { + "value": "450329", + "label": "资源县", + "children": [] + }] + }, { + "value": "451200", + "label": "河池市", + "children": [{ + "value": "451227", + "label": "巴马瑶族自治县", + "children": [] + }, { + "value": "451229", + "label": "大化瑶族自治县", + "children": [] + }, { + "value": "451224", + "label": "东兰县", + "children": [] + }, { + "value": "451228", + "label": "都安瑶族自治县", + "children": [] + }, { + "value": "451223", + "label": "凤山县", + "children": [] + }, { + "value": "451226", + "label": "环江毛南族自治县", + "children": [] + }, { + "value": "451202", + "label": "金城江区", + "children": [] + }, { + "value": "451225", + "label": "罗城仫佬族自治县", + "children": [] + }, { + "value": "451221", + "label": "南丹县", + "children": [] + }, { + "value": "451282", + "label": "其它区", + "children": [] + }, { + "value": "451222", + "label": "天峨县", + "children": [] + }, { + "value": "451281", + "label": "宜州市", + "children": [] + }] + }, { + "value": "451100", + "label": "贺州市", + "children": [{ + "value": "451102", + "label": "八步区", + "children": [] + }, { + "value": "451123", + "label": "富川瑶族自治县", + "children": [] + }, { + "value": "451119", + "label": "平桂管理区", + "children": [] + }, { + "value": "451124", + "label": "其它区", + "children": [] + }, { + "value": "451121", + "label": "昭平县", + "children": [] + }, { + "value": "451122", + "label": "钟山县", + "children": [] + }] + }, { + "value": "451300", + "label": "来宾市", + "children": [{ + "value": "451381", + "label": "合山市", + "children": [] + }, { + "value": "451324", + "label": "金秀瑶族自治县", + "children": [] + }, { + "value": "451382", + "label": "其它区", + "children": [] + }, { + "value": "451323", + "label": "武宣县", + "children": [] + }, { + "value": "451322", + "label": "象州县", + "children": [] + }, { + "value": "451321", + "label": "忻城县", + "children": [] + }, { + "value": "451302", + "label": "兴宾区", + "children": [] + }] + }, { + "value": "450200", + "label": "柳州市", + "children": [{ + "value": "450202", + "label": "城中区", + "children": [] + }, { + "value": "450205", + "label": "柳北区", + "children": [] + }, { + "value": "450222", + "label": "柳城县", + "children": [] + }, { + "value": "450221", + "label": "柳江县", + "children": [] + }, { + "value": "450204", + "label": "柳南区", + "children": [] + }, { + "value": "450223", + "label": "鹿寨县", + "children": [] + }, { + "value": "450227", + "label": "其它区", + "children": [] + }, { + "value": "450224", + "label": "融安县", + "children": [] + }, { + "value": "450225", + "label": "融水苗族自治县", + "children": [] + }, { + "value": "450226", + "label": "三江侗族自治县", + "children": [] + }, { + "value": "450203", + "label": "鱼峰区", + "children": [] + }] + }, { + "value": "450100", + "label": "南宁市", + "children": [{ + "value": "450126", + "label": "宾阳县", + "children": [] + }, { + "value": "450127", + "label": "横县", + "children": [] + }, { + "value": "450105", + "label": "江南区", + "children": [] + }, { + "value": "450108", + "label": "良庆区", + "children": [] + }, { + "value": "450123", + "label": "隆安县", + "children": [] + }, { + "value": "450124", + "label": "马山县", + "children": [] + }, { + "value": "450128", + "label": "其它区", + "children": [] + }, { + "value": "450103", + "label": "青秀区", + "children": [] + }, { + "value": "450125", + "label": "上林县", + "children": [] + }, { + "value": "450122", + "label": "武鸣区", + "children": [] + }, { + "value": "450107", + "label": "西乡塘区", + "children": [] + }, { + "value": "450102", + "label": "兴宁区", + "children": [] + }, { + "value": "450109", + "label": "邕宁区", + "children": [] + }] + }, { + "value": "450700", + "label": "钦州市", + "children": [{ + "value": "450721", + "label": "灵山县", + "children": [] + }, { + "value": "450722", + "label": "浦北县", + "children": [] + }, { + "value": "450723", + "label": "其它区", + "children": [] + }, { + "value": "450703", + "label": "钦北区", + "children": [] + }, { + "value": "450702", + "label": "钦南区", + "children": [] + }] + }, { + "value": "450400", + "label": "梧州市", + "children": [{ + "value": "450421", + "label": "苍梧县", + "children": [] + }, { + "value": "450481", + "label": "岑溪市", + "children": [] + }, { + "value": "450405", + "label": "长洲区", + "children": [] + }, { + "value": "450404", + "label": "蝶山区", + "children": [] + }, { + "value": "450406", + "label": "龙圩区", + "children": [] + }, { + "value": "450423", + "label": "蒙山县", + "children": [] + }, { + "value": "450482", + "label": "其它区", + "children": [] + }, { + "value": "450422", + "label": "藤县", + "children": [] + }, { + "value": "450403", + "label": "万秀区", + "children": [] + }] + }, { + "value": "450900", + "label": "玉林市", + "children": [{ + "value": "450981", + "label": "北流市", + "children": [] + }, { + "value": "450923", + "label": "博白县", + "children": [] + }, { + "value": "450903", + "label": "福绵区", + "children": [] + }, { + "value": "450922", + "label": "陆川县", + "children": [] + }, { + "value": "450982", + "label": "其它区", + "children": [] + }, { + "value": "450921", + "label": "容县", + "children": [] + }, { + "value": "450924", + "label": "兴业县", + "children": [] + }, { + "value": "450902", + "label": "玉州区", + "children": [] + }] + }] +}, { + "value": "810000", + "label": "香港特别行政区", + "children": [{ + "value": "810200", + "label": "九龙", + "children": [{ + "value": "810205", + "label": "观塘区", + "children": [] + }, { + "value": "810204", + "label": "黄大仙区", + "children": [] + }, { + "value": "810201", + "label": "九龙城区", + "children": [] + }, { + "value": "810203", + "label": "深水埗区", + "children": [] + }, { + "value": "810202", + "label": "油尖旺区", + "children": [] + }] + }, { + "value": "810100", + "label": "香港岛", + "children": [{ + "value": "810103", + "label": "东区", + "children": [] + }, { + "value": "810104", + "label": "南区", + "children": [] + }, { + "value": "810102", + "label": "湾仔", + "children": [] + }, { + "value": "810101", + "label": "中西区", + "children": [] + }] + }, { + "value": "810300", + "label": "新界", + "children": [{ + "value": "810301", + "label": "北区", + "children": [] + }, { + "value": "810302", + "label": "大埔区", + "children": [] + }, { + "value": "810308", + "label": "葵青区", + "children": [] + }, { + "value": "810309", + "label": "离岛区", + "children": [] + }, { + "value": "810307", + "label": "荃湾区", + "children": [] + }, { + "value": "810303", + "label": "沙田区", + "children": [] + }, { + "value": "810306", + "label": "屯门区", + "children": [] + }, { + "value": "810304", + "label": "西贡区", + "children": [] + }, { + "value": "810305", + "label": "元朗区", + "children": [] + }] + }] +}, { + "value": "330000", + "label": "浙江省", + "children": [{ + "value": "330100", + "label": "杭州市", + "children": [{ + "value": "330108", + "label": "滨江区", + "children": [] + }, { + "value": "330127", + "label": "淳安县", + "children": [] + }, { + "value": "330183", + "label": "富阳区", + "children": [] + }, { + "value": "330105", + "label": "拱墅区", + "children": [] + }, { + "value": "330182", + "label": "建德市", + "children": [] + }, { + "value": "330104", + "label": "江干区", + "children": [] + }, { + "value": "330185", + "label": "临安市", + "children": [] + }, { + "value": "330186", + "label": "其它区", + "children": [] + }, { + "value": "330102", + "label": "上城区", + "children": [] + }, { + "value": "330122", + "label": "桐庐县", + "children": [] + }, { + "value": "330106", + "label": "西湖区", + "children": [] + }, { + "value": "330103", + "label": "下城区", + "children": [] + }, { + "value": "330109", + "label": "萧山区", + "children": [] + }, { + "value": "330110", + "label": "余杭区", + "children": [] + }] + }, { + "value": "330500", + "label": "湖州市", + "children": [{ + "value": "330523", + "label": "安吉县", + "children": [] + }, { + "value": "330522", + "label": "长兴县", + "children": [] + }, { + "value": "330521", + "label": "德清县", + "children": [] + }, { + "value": "330503", + "label": "南浔区", + "children": [] + }, { + "value": "330524", + "label": "其它区", + "children": [] + }, { + "value": "330502", + "label": "吴兴区", + "children": [] + }] + }, { + "value": "330400", + "label": "嘉兴市", + "children": [{ + "value": "330481", + "label": "海宁市", + "children": [] + }, { + "value": "330424", + "label": "海盐县", + "children": [] + }, { + "value": "330421", + "label": "嘉善县", + "children": [] + }, { + "value": "330402", + "label": "南湖区", + "children": [] + }, { + "value": "330482", + "label": "平湖市", + "children": [] + }, { + "value": "330484", + "label": "其它区", + "children": [] + }, { + "value": "330483", + "label": "桐乡市", + "children": [] + }, { + "value": "330411", + "label": "秀洲区", + "children": [] + }] + }, { + "value": "330700", + "label": "金华市", + "children": [{ + "value": "330783", + "label": "东阳市", + "children": [] + }, { + "value": "330703", + "label": "金东区", + "children": [] + }, { + "value": "330781", + "label": "兰溪市", + "children": [] + }, { + "value": "330727", + "label": "磐安县", + "children": [] + }, { + "value": "330726", + "label": "浦江县", + "children": [] + }, { + "value": "330785", + "label": "其它区", + "children": [] + }, { + "value": "330702", + "label": "婺城区", + "children": [] + }, { + "value": "330723", + "label": "武义县", + "children": [] + }, { + "value": "330782", + "label": "义乌市", + "children": [] + }, { + "value": "330784", + "label": "永康市", + "children": [] + }] + }, { + "value": "331100", + "label": "丽水市", + "children": [{ + "value": "331122", + "label": "缙云县", + "children": [] + }, { + "value": "331127", + "label": "景宁畲族自治县", + "children": [] + }, { + "value": "331102", + "label": "莲都区", + "children": [] + }, { + "value": "331181", + "label": "龙泉市", + "children": [] + }, { + "value": "331182", + "label": "其它区", + "children": [] + }, { + "value": "331121", + "label": "青田县", + "children": [] + }, { + "value": "331126", + "label": "庆元县", + "children": [] + }, { + "value": "331124", + "label": "松阳县", + "children": [] + }, { + "value": "331123", + "label": "遂昌县", + "children": [] + }, { + "value": "331125", + "label": "云和县", + "children": [] + }] + }, { + "value": "330200", + "label": "宁波市", + "children": [{ + "value": "330206", + "label": "北仑区", + "children": [] + }, { + "value": "330282", + "label": "慈溪市", + "children": [] + }, { + "value": "330283", + "label": "奉化市", + "children": [] + }, { + "value": "330203", + "label": "海曙区", + "children": [] + }, { + "value": "330205", + "label": "江北区", + "children": [] + }, { + "value": "330204", + "label": "江东区", + "children": [] + }, { + "value": "330226", + "label": "宁海县", + "children": [] + }, { + "value": "330284", + "label": "其它区", + "children": [] + }, { + "value": "330225", + "label": "象山县", + "children": [] + }, { + "value": "330212", + "label": "鄞州区", + "children": [] + }, { + "value": "330281", + "label": "余姚市", + "children": [] + }, { + "value": "330211", + "label": "镇海区", + "children": [] + }] + }, { + "value": "330800", + "label": "衢州市", + "children": [{ + "value": "330822", + "label": "常山县", + "children": [] + }, { + "value": "330881", + "label": "江山市", + "children": [] + }, { + "value": "330824", + "label": "开化县", + "children": [] + }, { + "value": "330802", + "label": "柯城区", + "children": [] + }, { + "value": "330825", + "label": "龙游县", + "children": [] + }, { + "value": "330882", + "label": "其它区", + "children": [] + }, { + "value": "330803", + "label": "衢江区", + "children": [] + }] + }, { + "value": "330600", + "label": "绍兴市", + "children": [{ + "value": "330621", + "label": "柯桥区", + "children": [] + }, { + "value": "330684", + "label": "其它区", + "children": [] + }, { + "value": "330682", + "label": "上虞区", + "children": [] + }, { + "value": "330683", + "label": "嵊州市", + "children": [] + }, { + "value": "330624", + "label": "新昌县", + "children": [] + }, { + "value": "330602", + "label": "越城区", + "children": [] + }, { + "value": "330681", + "label": "诸暨市", + "children": [] + }] + }, { + "value": "331000", + "label": "台州市", + "children": [{ + "value": "331003", + "label": "黄岩区", + "children": [] + }, { + "value": "331002", + "label": "椒江区", + "children": [] + }, { + "value": "331082", + "label": "临海市", + "children": [] + }, { + "value": "331004", + "label": "路桥区", + "children": [] + }, { + "value": "331083", + "label": "其它区", + "children": [] + }, { + "value": "331022", + "label": "三门县", + "children": [] + }, { + "value": "331023", + "label": "天台县", + "children": [] + }, { + "value": "331081", + "label": "温岭市", + "children": [] + }, { + "value": "331024", + "label": "仙居县", + "children": [] + }, { + "value": "331021", + "label": "玉环县", + "children": [] + }] + }, { + "value": "330300", + "label": "温州市", + "children": [{ + "value": "330327", + "label": "苍南县", + "children": [] + }, { + "value": "330322", + "label": "洞头县", + "children": [] + }, { + "value": "330303", + "label": "龙湾区", + "children": [] + }, { + "value": "330302", + "label": "鹿城区", + "children": [] + }, { + "value": "330304", + "label": "瓯海区", + "children": [] + }, { + "value": "330326", + "label": "平阳县", + "children": [] + }, { + "value": "330383", + "label": "其它区", + "children": [] + }, { + "value": "330381", + "label": "瑞安市", + "children": [] + }, { + "value": "330329", + "label": "泰顺县", + "children": [] + }, { + "value": "330328", + "label": "文成县", + "children": [] + }, { + "value": "330324", + "label": "永嘉县", + "children": [] + }, { + "value": "330382", + "label": "乐清市", + "children": [] + }] + }, { + "value": "330900", + "label": "舟山市", + "children": [{ + "value": "330921", + "label": "岱山县", + "children": [] + }, { + "value": "330902", + "label": "定海区", + "children": [] + }, { + "value": "330903", + "label": "普陀区", + "children": [] + }, { + "value": "330923", + "label": "其它区", + "children": [] + }, { + "value": "330922", + "label": "嵊泗县", + "children": [] + }] + }] +}]] \ No newline at end of file diff --git a/components/picker/demo/data/simple.js b/components/picker/demo/data/simple.js new file mode 100644 index 00000000..65653d5d --- /dev/null +++ b/components/picker/demo/data/simple.js @@ -0,0 +1,20 @@ +export default [ + [ + {text: '2015', value: 1}, + {text: '2016', value: 2}, + {text: '2017', value: 3}, + {text: '2018', value: 4}, + {text: '2019', value: 5}, + {text: '2020', value: 6}, + {text: '2021', value: 2}, + {text: '2022', value: 3}, + {text: '2023', value: 2}, + {text: '2024', value: 3}, + {text: '2025', value: 2}, + {text: '2026', value: 3}, + {text: '2027', value: 2}, + {text: '2028', value: 3}, + {text: '2029', value: 2}, + {text: '2030', value: 3}, + ], +] diff --git a/components/picker/demo/index.vue b/components/picker/demo/index.vue new file mode 100644 index 00000000..6d4f968d --- /dev/null +++ b/components/picker/demo/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/picker/index.vue b/components/picker/index.vue new file mode 100644 index 00000000..57cacf0c --- /dev/null +++ b/components/picker/index.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/components/picker/picker-column.vue b/components/picker/picker-column.vue new file mode 100644 index 00000000..e6f913ca --- /dev/null +++ b/components/picker/picker-column.vue @@ -0,0 +1,478 @@ + + + + + diff --git a/components/picker/test/index.spec.js b/components/picker/test/index.spec.js new file mode 100644 index 00000000..2673d0cb --- /dev/null +++ b/components/picker/test/index.spec.js @@ -0,0 +1,95 @@ +import Picker from '../index' +import PickerColumn from '../picker-column' +import triggerTouch from '../../popup/test/touch-trigger' +import simple from '../demo/data/simple' +import district from '../demo/data/district' +import {mount} from 'avoriaz' + +describe('Picker', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a picker', done => { + wrapper = mount(Picker) + expect(wrapper.vm.data.length).to.equal(0) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.data = simple + wrapper.vm.value = true + setTimeout(() => { + expect(eventStub.calledWith('show')).to.be.true + expect(wrapper.vm.isPickerShow).to.be.true + expect(!!wrapper.vm.column).to.be.true + expect(wrapper.find('.column-item').length).to.equal(16) + const cancelmBtn = wrapper.find('.md-popup-cancel')[0] + cancelmBtn.trigger('click') + wrapper.vm.value = false + expect(eventStub.calledWith('cancel')).to.be.true + setTimeout(() => { + expect(eventStub.calledWith('hide')).to.be.true + done() + }, 300) + }, 300) + }) + + it('create a picker column', done => { + wrapper = mount(PickerColumn) + expect(wrapper.vm.data.length).to.equal(0) + expect(wrapper.vm.defaultIndex.length).to.equal(0) + expect(wrapper.vm.defaultValue.length).to.equal(0) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.data = simple + wrapper.vm.validIndex = [3] + wrapper.vm.defaultIndex = [2] + wrapper.vm.refresh(() => { + expect(wrapper.vm.getColumnIndex(0)).to.equal(2) + expect(wrapper.vm.getColumnIndexs()[0]).to.equal(2) + expect(wrapper.vm.getColumnValue(0).text).to.equal('2017') + + const hook = wrapper.find('.md-picker-column-hook')[0] + + triggerTouch(hook.element, 'touchstart', 0, 0) + triggerTouch(hook.element, 'touchmove', 0, 108) + triggerTouch(hook.element, 'touchend') + // expect(eventStub.calledWith('change')).to.be.true + + wrapper.vm.setColumnValues(0, simple[0]) + done() + }) + }) + + it('picker defalut index', done => { + wrapper = mount(Picker) + expect(wrapper.vm.data.length).to.equal(0) + + wrapper.vm.defaultIndex = [1] + wrapper.vm.data = simple + wrapper.vm.value = true + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.column-item').length).to.equal(16) + done() + }) + }) + + it('picker cascade', done => { + wrapper = mount(Picker, { + propsData: { + isCascade: true, + }, + }) + wrapper.vm.data = district + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.vm.$nextTick(() => { + const confirmBtn = wrapper.find('.md-popup-confirm')[0] + confirmBtn.trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }) + }) +}) diff --git a/components/popup-title-bar/index.vue b/components/popup-title-bar/index.vue new file mode 100644 index 00000000..2760e1a8 --- /dev/null +++ b/components/popup-title-bar/index.vue @@ -0,0 +1,4 @@ + + diff --git a/components/popup/README.md b/components/popup/README.md new file mode 100644 index 00000000..73c26288 --- /dev/null +++ b/components/popup/README.md @@ -0,0 +1,60 @@ +--- +title: Popup 弹出层 +preview: https://didi.github.io/mand-mobile/examples/popup +--- + +由其他控件触发,屏幕滑出或弹出一块自定义内容区域 + +### 引入 + +```javascript +import { Popup, PopupTitleBar } from 'mand-mobile' + +Vue.component(Popup.name, Popup) +Vue.component(PopupTitleBar.name, PopupTitleBar) +``` + +### 代码演示 + + +### API + +#### Popup Props +|属性 | 说明 | 类型 | 默认值| 备注| +|----|-----|------|------|------| +|v-model|弹出层是否可见|Boolean|`false`|-| +|has-mask|是否有蒙层|Boolean|`true`|-| +|mask-closable|点击蒙层是否可关闭弹出层|Boolean|`true`|-| +|position|弹出层位置|String|`center`|`center`, `top`, `bottom`, `left`, `right`| +|transition|弹出层过度动画|String|`fade, slide-up/down/left/right`|-| +|prevent-scroll|是否禁止滚动穿透|Boolean|`false`|-| +|prevent-scroll-exclude|禁止滚动的排除元素|String/HTMLElement|-|-| + +#### PopupTitleBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|title|标题|String|-|-| +|ok-text|确认按钮文案|String|-|为空则没有确认按钮| +|cancel-text|取消按钮文案|String|-|为空则没有取消按钮| + +#### Popup Events + +#### @beforeShow() +弹出层即将展示事件 + +#### @show() +弹出层展示事件 + +#### @beforeHide() +弹出层即将隐藏事件 + +#### @hide() +弹出层隐藏事件 + +#### PopupTitleBar Events + +##### @confirm() +确认选择事件 + +##### @cancel() +取消选择事件 diff --git a/components/popup/component.js b/components/popup/component.js new file mode 100644 index 00000000..76a1923c --- /dev/null +++ b/components/popup/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'popup', + 'text': '弹出层', + 'category': 'feedback', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/popup/demo/cases/demo0.vue b/components/popup/demo/cases/demo0.vue new file mode 100644 index 00000000..a16ec39b --- /dev/null +++ b/components/popup/demo/cases/demo0.vue @@ -0,0 +1,128 @@ + + + + + \ No newline at end of file diff --git a/components/popup/demo/cases/demo1.vue b/components/popup/demo/cases/demo1.vue new file mode 100644 index 00000000..371a5632 --- /dev/null +++ b/components/popup/demo/cases/demo1.vue @@ -0,0 +1,124 @@ + + +mand-mobile +import {Popup, PopupTitleBar, Button, Icon} from 'mand-mobile' + +export default { + name: 'popup-demo', + title: '其他配置', + message: '防止滚动击穿请在移动设备中扫码预览', + height: 750, + components: { + [Popup.name]: Popup, + [PopupTitleBar.name]: PopupTitleBar, + [Button.name]: Button, + [Icon.name]: Icon, + }, + data() { + return { + isPopupShow: {}, + } + }, + methods: { + showPopUp(type) { + this.$set(this.isPopupShow, type, true) + }, + hidePopUp(type) { + this.$set(this.isPopupShow, type, false) + }, + }, +} + + + + \ No newline at end of file diff --git a/components/popup/demo/index.vue b/components/popup/demo/index.vue new file mode 100644 index 00000000..a1e8db88 --- /dev/null +++ b/components/popup/demo/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/components/popup/index.vue b/components/popup/index.vue new file mode 100644 index 00000000..021a8165 --- /dev/null +++ b/components/popup/index.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/components/popup/test/index.spec.js b/components/popup/test/index.spec.js new file mode 100644 index 00000000..d19fdc00 --- /dev/null +++ b/components/popup/test/index.spec.js @@ -0,0 +1,107 @@ +import Popup from '../index' +import PopupTitleBar from '../title-bar' +import {mount} from 'avoriaz' +import triggerTouch from './touch-trigger' + +describe('Popup', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a popup', done => { + wrapper = mount(Popup, { + propsData: { + value: true, + }, + slots: { + default: PopupTitleBar, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.isPopupShow).to.be.true + expect(wrapper.vm.isPopupBoxShow).to.be.true + wrapper.vm.value = false + done() + }) + }) + + it('create a popup with position center', done => { + wrapper = mount(Popup, { + propsData: { + position: 'center', + }, + }) + expect(wrapper.vm.transition).to.equal('fade') + wrapper.vm.value = true + setTimeout(() => { + const popupBox = wrapper.find('.md-popup-box')[0] + expect(wrapper.hasClass('md-popup') && wrapper.hasClass('center') && popupBox.hasClass('fade')).to.be.true + done() + }, 300) + }) + + it('popup with transition', done => { + wrapper = mount(Popup, { + propsData: { + position: 'bottom', + }, + }) + const popupBox = wrapper.find('.md-popup-box')[0] + + expect(wrapper.vm.transition).to.equal('slide-up') + + wrapper.vm.transition = 'slide-for-test' + // expect(popupBox.hasClass('slide-for-test')).to.be.true + done() + }) + + it('popup without mask', done => { + wrapper = mount(Popup, { + propsData: { + hasMask: false, + }, + }) + + wrapper.vm.value = true + setTimeout(() => { + expect(wrapper.contains('.md-popup-mask')).to.be.true + done() + }, 300) + }) + + it('popup mask is closable', done => { + wrapper = mount(Popup) + expect(wrapper.hasClass('with-mask')).to.be.true + + wrapper.vm.value = true + setTimeout(() => { + const popupMask = wrapper.find('.md-popup-mask')[0] + popupMask.trigger('click') + expect(wrapper.vm.isPopupBoxShow).to.be.false + done() + }, 300) + }) + + it('popup prevent scroll', done => { + wrapper = mount(Popup, { + propsData: { + preventScroll: true, + value: true, + }, + }) + + const popupBox = wrapper.find('.md-popup-box')[0] + setTimeout(() => { + document.body.style.height = '10000px' + triggerTouch(popupBox.element, 'touchstart', 0, 0) + triggerTouch(popupBox.element, 'touchmove', 0, 100) + triggerTouch(document, 'touchstart', 0, 0) + triggerTouch(document, 'touchmove', 0, 100) + document.body.style.height = '' + done() + }, 300) + }) +}) diff --git a/components/popup/test/touch-trigger.js b/components/popup/test/touch-trigger.js new file mode 100644 index 00000000..70cb2268 --- /dev/null +++ b/components/popup/test/touch-trigger.js @@ -0,0 +1,23 @@ +export default function(element, eventName, x, y, keyCode) { + const touch = { + identifier: Date.now(), + target: element, + pageX: x, + pageY: y, + clientX: x, + clientY: y, + radiusX: 2.5, + radiusY: 2.5, + rotationAngle: 10, + force: 0.5, + } + + const event = document.createEvent('CustomEvent') + event.initCustomEvent(eventName, true, true, {}) + event.touches = [touch] + event.targetTouches = [touch] + event.changedTouches = [touch] + event.keyCode = keyCode + + element.dispatchEvent(event) +} diff --git a/components/popup/title-bar.vue b/components/popup/title-bar.vue new file mode 100644 index 00000000..b9ebae02 --- /dev/null +++ b/components/popup/title-bar.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/components/radio/README.md b/components/radio/README.md new file mode 100644 index 00000000..7848b229 --- /dev/null +++ b/components/radio/README.md @@ -0,0 +1,89 @@ +--- +title: Radio 单选框 +preview: https://didi.github.io/mand-mobile/examples/radio +--- + +可自定义或编辑单选框 + +### 引入 + +```javascript +import { Radio } from 'mand-mobile' + +Vue.component(Radio.name, Radio) +``` + +### 代码演示 + + +### API + +#### Radio Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|选中项的`value`|String|-|如果数据源中没有`value`, 则为`text`或`label`| +|options|选项数据源|Array<{text, value, disabled, ...}>|`[]`|`disabled`为选项是否禁用| +|default-index|默认选择项索引|Number|`-1`|`v-model`有初始值时无效| +|invalid-index|禁用选择项索引|Number/Array|`-1`|作用等同于`options`元素中的属性`disabled`| +|has-input-option|是否具有可编辑项|Boolean|`false`|-| +|input-option-label|可编辑项的名称|String|-|仅用于`has-input-option`为`true`| +|input-option-placeholder|可编辑项的占位提示|String|-|仅用于`has-input-option`为`true`| +|icon|选中项的图标|String|`right`|-| +|icon-inverse|非选中项的图标|String|-|-| +|icon-size|图标大小|String|`sm`|-| +|icon-position|图标位置|String|`right`|`left`, `right`| +|option-render|返回各选项自定义渲染内容|Function({text, value, disabled, ...}): String|-|`vue 2.1.0+`可使用`slot-scope`,见附录| +|is-slot-scope|是否强制使用或不使用`slot-scope`|Boolean|-|某些情况下需要根据业务逻辑动态确定是否使用| + +#### Radio Methods + +##### getSelectedValue(): option +获取当前选中项 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|option|选中项的数据|`Object:{text, value, disabled, ...}`,如果选中为可编辑项,则为`String`| + +##### getSelectedIndex(): index +获取当前选中项索引值 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|index|选中项索引值|Number| + +##### selectByIndex(index) +设置选中项 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|选中项索引值|Number| + +#### Component Events + +##### @change(option, index) +切换选中项事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|option|选中项的数据|`Object:{text, value, disabled, ...}`,如果选中为可编辑项,则为`String`| +|index|选中项索引值|Number| + +#### 附录 + +```html + +``` diff --git a/components/radio/component.js b/components/radio/component.js new file mode 100644 index 00000000..a76587c6 --- /dev/null +++ b/components/radio/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'radio', + 'text': '单选框', + 'category': 'form', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/radio/demo/cases/demo0.vue b/components/radio/demo/cases/demo0.vue new file mode 100644 index 00000000..419ec15f --- /dev/null +++ b/components/radio/demo/cases/demo0.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/components/radio/demo/cases/demo1.vue b/components/radio/demo/cases/demo1.vue new file mode 100644 index 00000000..a12ec844 --- /dev/null +++ b/components/radio/demo/cases/demo1.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/components/radio/demo/cases/demo2.vue b/components/radio/demo/cases/demo2.vue new file mode 100644 index 00000000..e3f8b2b5 --- /dev/null +++ b/components/radio/demo/cases/demo2.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/components/radio/demo/cases/demo3.vue b/components/radio/demo/cases/demo3.vue new file mode 100644 index 00000000..d7a2b5bf --- /dev/null +++ b/components/radio/demo/cases/demo3.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/components/radio/demo/cases/demo4.vue b/components/radio/demo/cases/demo4.vue new file mode 100644 index 00000000..b9a72fce --- /dev/null +++ b/components/radio/demo/cases/demo4.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/radio/demo/index.vue b/components/radio/demo/index.vue new file mode 100644 index 00000000..588eb27c --- /dev/null +++ b/components/radio/demo/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/radio/index.vue b/components/radio/index.vue new file mode 100644 index 00000000..0b1b3e0d --- /dev/null +++ b/components/radio/index.vue @@ -0,0 +1,293 @@ + + + + + + diff --git a/components/radio/test/index.spec.js b/components/radio/test/index.spec.js new file mode 100644 index 00000000..0c0cb67e --- /dev/null +++ b/components/radio/test/index.spec.js @@ -0,0 +1,95 @@ +import Radio from '../index' +import {mount} from 'avoriaz' + +describe('Radio', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a radio', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1', disabled: true}, {text: '选项2'}, {text: '选项3'}], + defaultIndex: 1, + }, + }) + + expect(wrapper.find('.md-field-item').length).to.equal(3) + expect(wrapper.vm.selectedIndex).to.equal(1) + + wrapper.vm.options = [{text: '选项1'}, {text: '选项2'}] + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-field-item').length).to.equal(2) + done() + }) + }) + + it('create a radio with initial value', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1', disabled: true}, {text: '选项2'}, {text: '选项3'}], + invalidIndex: 1, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = '选项2' + wrapper.vm.$nextTick(() => { + expect(eventStub.calledWith('input')).to.be.true + wrapper.vm.value = '选项1' + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.selectedIndex).to.equal(0) + + wrapper.instance().selectByIndex(4) + wrapper.instance().selectByIndex(1) + expect(wrapper.vm.selectedIndex).to.equal(0) + wrapper.instance().selectByIndex(2) + expect(wrapper.vm.selectedIndex).to.equal(2) + done() + }) + }) + }) + + it('create a radio with input', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1', disabled: true}, {text: '选项2'}, {text: '选项3'}], + hasInputOption: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + const option = wrapper.find('.md-input-item')[0] + expect(!!option).to.be.true + option.find('input')[0].trigger('focus') + wrapper.vm.inputOptionValue = '123' + expect(eventStub.calledWith('input')).to.be.true + option.find('input')[0].trigger('blur') + expect(wrapper.vm.selectedIndex).to.equal(3) + expect(wrapper.instance().getSelectedIndex()).to.equal(3) + expect(wrapper.instance().getSelectedValue()).to.equal('123') + wrapper.instance().selectByIndex(3) + done() + }) + + it('radio option choose', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1'}, {text: '选项2'}, {text: '选项3'}], + invalidIndex: 1, + }, + }) + + const options = wrapper.find('.md-field-item') + options[0].trigger('click') + options[1].trigger('click') + expect(wrapper.vm.selectedIndex).to.equal(0) + expect(wrapper.instance().getSelectedValue().text).to.equal('选项1') + expect(options[1].hasClass('disabled')).to.be.true + done() + }) +}) diff --git a/components/result-page/README.md b/components/result-page/README.md new file mode 100644 index 00000000..014325c8 --- /dev/null +++ b/components/result-page/README.md @@ -0,0 +1,39 @@ +--- +title: ResultPage 结果页 +preview: https://didi.github.io/mand-mobile/examples/result-page +--- + +用于展示流程结束页面的控件 + +### 引入 + +```javascript +import { ResultPage } from 'mand-mobile' + +Vue.component(ResultPage.name, ResultPage) +``` + +### 使用指南 + +建议将组建的父元素设置填满视窗,以达到居中的效果。页面上的图片会根据`type`设置相应的默认值 + +### 代码演示 + + +### API + +#### ResultPage Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|type | 页面类别 | String | `empty` | type可取`lost`, `network`和`empty`三个值,分别代表页面丢失、网络出错和空信息。根据类别不同,组件会拥有不同的默认图片和文案| +|img-url | 图片链接 | String | 空信息图片 | 根据类别不同,组件会拥有不同的默认图片 | +|text | 主文案 | String | `暂无信息` | 根据类别不同,组件会拥有不同的默认主文案 | +|subtext | 副文案 | String | - | 以更小的字体和更淡的颜色显示在主文案下方 | +|buttons | 按钮列表 | Array | - | 按钮对象数组,按钮对象结构可参考Button Props表| + +#### Button Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|text | 按钮文字 | String | - | - | +|type | 按钮样式类别 | String | `ghost` | 还可以选择`ghost-primary`,可参考`Button`控件 | +|handler | 点击操作 | Function | - | 点击按钮后调用的方法 | diff --git a/components/result-page/component.js b/components/result-page/component.js new file mode 100644 index 00000000..b16dff52 --- /dev/null +++ b/components/result-page/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'result-page', + 'text': '结果页', + 'category': 'business', + 'description': '用于展示流程终点页面的组件。', + 'author': 'zhaozhe' +} diff --git a/components/result-page/demo/cases/demo0.vue b/components/result-page/demo/cases/demo0.vue new file mode 100644 index 00000000..99a0f45d --- /dev/null +++ b/components/result-page/demo/cases/demo0.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/cases/demo1.vue b/components/result-page/demo/cases/demo1.vue new file mode 100644 index 00000000..6ece03eb --- /dev/null +++ b/components/result-page/demo/cases/demo1.vue @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/cases/demo2.vue b/components/result-page/demo/cases/demo2.vue new file mode 100644 index 00000000..df18fc2e --- /dev/null +++ b/components/result-page/demo/cases/demo2.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/cases/demo3.vue b/components/result-page/demo/cases/demo3.vue new file mode 100644 index 00000000..c67188c3 --- /dev/null +++ b/components/result-page/demo/cases/demo3.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/index.vue b/components/result-page/demo/index.vue new file mode 100644 index 00000000..0bba7dd6 --- /dev/null +++ b/components/result-page/demo/index.vue @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/index.vue b/components/result-page/index.vue new file mode 100644 index 00000000..3050a94c --- /dev/null +++ b/components/result-page/index.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/components/result-page/test/index.spec.js b/components/result-page/test/index.spec.js new file mode 100644 index 00000000..84135849 --- /dev/null +++ b/components/result-page/test/index.spec.js @@ -0,0 +1,16 @@ +import ResultPage from '../index' +import {mount} from 'avoriaz' + +describe('ResultPage', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple result-page', () => { + wrapper = mount(ResultPage) + + expect(wrapper.hasClass('md-result-page')).to.be.true + }) +}) diff --git a/components/selector/README.md b/components/selector/README.md new file mode 100644 index 00000000..be813d2f --- /dev/null +++ b/components/selector/README.md @@ -0,0 +1,51 @@ +--- +title: Selector 列表选择器 +preview: https://didi.github.io/mand-mobile/examples/selector +--- + +用于弹出列表中选择一项 + +### 引入 + +```javascript +import { Selector } from 'mand-mobile' + +Vue.component(Selector.name, Selector) +``` + +### 代码演示 + + + +### API + +#### Selector Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|选择器是否可见|Boolean|false|-| +|data|数据源|Array<{value,text,...}>|`[]`|`label`可为`html`片段| +|default-index|选择器初始选中项索引|Number|-|-| +|invalid-index|选择器不可用选项索引|Number|-|-| +|title|选择器标题|String|-|-| +|ok-text|选择器确认文案|String|-|若为空则为`确认模式`,即点击选项直接选择| +|cancel-text|选择器取消文案|String|`取消`|-| +|is-check|是否有`check`图标|Boolean|`false`|仅`确认模式`| +|option-render|返回各选项渲染内容|Function({value, text ,...}):String|-|`vue 2.1.0+`可使用`slot-scope`,参考`Radio`| + + +#### Selector Events + +#### @choose({value, text, ...}) +选择器选中某选项事件 + +#### @confirm({value, text, ...}) +选择器确认选中事件 + +#### @cancel() +选择器取消选中事件 + +#### @show() +选择器展示事件 + +#### @hide() +选择器隐藏事件 diff --git a/components/selector/component.js b/components/selector/component.js new file mode 100644 index 00000000..7b687ba5 --- /dev/null +++ b/components/selector/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'selector', + 'text': '列表选择器', + 'category': 'feedback', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/selector/demo/cases/demo0.vue b/components/selector/demo/cases/demo0.vue new file mode 100644 index 00000000..1ea2e1dc --- /dev/null +++ b/components/selector/demo/cases/demo0.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/components/selector/demo/cases/demo1.vue b/components/selector/demo/cases/demo1.vue new file mode 100644 index 00000000..ddd730bc --- /dev/null +++ b/components/selector/demo/cases/demo1.vue @@ -0,0 +1,80 @@ + + + + + \ No newline at end of file diff --git a/components/selector/demo/cases/demo2.vue b/components/selector/demo/cases/demo2.vue new file mode 100644 index 00000000..c5957249 --- /dev/null +++ b/components/selector/demo/cases/demo2.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/components/selector/demo/cases/demo3.vue b/components/selector/demo/cases/demo3.vue new file mode 100644 index 00000000..e6e3523d --- /dev/null +++ b/components/selector/demo/cases/demo3.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/components/selector/demo/index.vue b/components/selector/demo/index.vue new file mode 100644 index 00000000..3c74598a --- /dev/null +++ b/components/selector/demo/index.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/components/selector/index.vue b/components/selector/index.vue new file mode 100644 index 00000000..90970c3a --- /dev/null +++ b/components/selector/index.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/components/selector/test/index.spec.js b/components/selector/test/index.spec.js new file mode 100644 index 00000000..ac27f324 --- /dev/null +++ b/components/selector/test/index.spec.js @@ -0,0 +1,180 @@ +// import Vue from 'vue' +import Selector from '../index' +import {mount} from 'avoriaz' +import {setTimeout} from 'timers' + +describe('Selector', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a selector', done => { + wrapper = mount(Selector) + + expect(wrapper.hasClass('md-selector')).to.be.true + expect(wrapper.vm.data.length).to.equal(0) + + wrapper.vm.value = true + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-radio-item').length).to.equal(4) + done() + }) + }) + + it('create a selector as check mode', () => { + wrapper = mount(Selector, { + propsData: { + okText: '确认', + isCheck: true, + }, + }) + + expect(wrapper.hasClass('is-check')).to.be.true + expect(wrapper.vm.isNeedConfirm).to.equal(true) + }) + + it('create a selector with default and invalid', done => { + wrapper = mount(Selector) + + wrapper.vm.value = true + wrapper.vm.defaultIndex = 1 + wrapper.vm.invalidIndex = 2 + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-radio-item')[1].hasClass('selected')).to.equal(true) + expect(wrapper.find('.md-radio-item')[2].hasClass('disabled')).to.equal(true) + done() + }) + }) + + it('selector events choose', done => { + wrapper = mount(Selector) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = true + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + + wrapper.vm.$nextTick(() => { + wrapper.find('.md-radio-item')[0].trigger('click') + expect(wrapper.vm.tmpActiveIndex).equal(0) + expect(wrapper.vm.activeIndex).equal(0) + expect(eventStub.calledWith('choose')).to.be.true + done() + }) + }) + + it('selector events confirm', done => { + wrapper = mount(Selector) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = true + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + wrapper.vm.okText = 'ok' + wrapper.vm.$nextTick(() => { + const item = wrapper.find('.md-radio-item')[0] + const confirmBtn = wrapper.find('.md-popup-confirm')[0] + item.trigger('click') + expect(wrapper.vm.tmpActiveIndex).equal(0) + + confirmBtn.trigger('click') + expect(wrapper.vm.activeIndex).equal(0) + expect(eventStub.calledWith('confirm')).to.be.true + done() + }) + }) + + it('selector events cancel', done => { + wrapper = mount(Selector, { + propsData: { + data: [ + [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ], + ], + okText: 'ok', + cancelText: 'cancel', + defaultIndex: 2, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = true + wrapper.vm.$nextTick(() => { + const cancelBtn = wrapper.find('.md-popup-cancel')[0] + cancelBtn.trigger('click') + expect(eventStub.calledWith('cancel')).to.be.true + done() + }) + }) +}) diff --git a/components/stepper/README.md b/components/stepper/README.md new file mode 100644 index 00000000..b188ba55 --- /dev/null +++ b/components/stepper/README.md @@ -0,0 +1,34 @@ +--- +title: Stepper 步进器 +preview: https://didi.github.io/mand-mobile/examples/stepper +--- + +增加,减少或修改当前数值 + +### 引入 + +```javascript +import { Stepper } from 'mand-mobile' + +Vue.component(Stepper.name, Stepper) +``` + +### 代码演示 + + +### API + +#### Stepper Props +属性 | 说明 | 类型 | 默认值 +---------|------|--------|---- +default-value |默认值| Number|- +step|每次改变步数,可以为小数|Number|`1` +min|最小值|Number|`-Infinity` +max|最大值|Number|`Infinity` +disabled|禁用| Boolean|`false` +read-only|只读| Boolean|`false` + +#### Stepper Events + +##### @change() +值发生变化事件 diff --git a/components/stepper/component.js b/components/stepper/component.js new file mode 100644 index 00000000..859a86cf --- /dev/null +++ b/components/stepper/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'stepper', + 'text': '步进器', + 'category': 'basic', + 'description': '步进器', + 'author': 'linyufei' +} diff --git a/components/stepper/demo/cases/demo0.vue b/components/stepper/demo/cases/demo0.vue new file mode 100644 index 00000000..a138dd7a --- /dev/null +++ b/components/stepper/demo/cases/demo0.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo1.vue b/components/stepper/demo/cases/demo1.vue new file mode 100644 index 00000000..5fd70f5d --- /dev/null +++ b/components/stepper/demo/cases/demo1.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo2.vue b/components/stepper/demo/cases/demo2.vue new file mode 100644 index 00000000..2a47c776 --- /dev/null +++ b/components/stepper/demo/cases/demo2.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo3.vue b/components/stepper/demo/cases/demo3.vue new file mode 100644 index 00000000..850594d4 --- /dev/null +++ b/components/stepper/demo/cases/demo3.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo4.vue b/components/stepper/demo/cases/demo4.vue new file mode 100644 index 00000000..b15543a7 --- /dev/null +++ b/components/stepper/demo/cases/demo4.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo5.vue b/components/stepper/demo/cases/demo5.vue new file mode 100644 index 00000000..5f25883e --- /dev/null +++ b/components/stepper/demo/cases/demo5.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/index.vue b/components/stepper/demo/index.vue new file mode 100644 index 00000000..5c043c3b --- /dev/null +++ b/components/stepper/demo/index.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/stepper/index.vue b/components/stepper/index.vue new file mode 100644 index 00000000..d664ed28 --- /dev/null +++ b/components/stepper/index.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/components/stepper/test/index.spec.js b/components/stepper/test/index.spec.js new file mode 100644 index 00000000..7049aec6 --- /dev/null +++ b/components/stepper/test/index.spec.js @@ -0,0 +1,111 @@ +import Stepper from '../index' +import {mount} from 'avoriaz' + +describe('Stepper', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple stepper', () => { + wrapper = mount(Stepper) + + expect(wrapper.hasClass('md-stepper')).to.be.true + expect(wrapper.vm.defaultValue).to.equal(0) + expect(wrapper.vm.step).to.equal(1) + expect(wrapper.vm.disabled).to.equal(false) + expect(wrapper.vm.readOnly).to.equal(false) + }) + + it('change stepper default props', () => { + wrapper = mount(Stepper, { + propsData: { + defaultValue: 2, + step: 2, + disabled: true, + readOnly: true, + }, + }) + + expect(wrapper.vm.defaultValue).to.equal(2) + expect(wrapper.vm.step).to.equal(2) + expect(wrapper.vm.disabled).to.equal(true) + expect(wrapper.vm.readOnly).to.equal(true) + }) + + it('stepper method reduce', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = 1 + wrapper.vm.step = 2 + wrapper.find('.md-stepper-button-reduce')[0].trigger('click') + expect(Number(wrapper.vm.currentNum)).to.equal(-1) + }) + + it('stepper method add', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = 1 + wrapper.vm.step = 2 + wrapper.find('.md-stepper-button-add')[0].trigger('click') + expect(wrapper.vm.currentNum).to.equal(3) + }) + + it('stepper method getCurrentNum', () => { + wrapper = mount(Stepper) + wrapper.vm.defaultValue = 2 + wrapper.vm.min = 3 + expect(wrapper.vm.$_getCurrentNum()).to.equal(wrapper.vm.min) + + wrapper.vm.defaultValue = 4 + wrapper.vm.max = 3 + expect(wrapper.vm.$_getCurrentNum()).to.equal(wrapper.vm.max) + }) + + it('stepper method checkMinMax', () => { + wrapper = mount(Stepper) + wrapper.vm.max = 5 + wrapper.vm.min = 3 + expect(wrapper.vm.$_checkMinMax()).to.equal(true) + + wrapper.vm.max = 3 + wrapper.vm.min = 5 + expect(wrapper.vm.$_checkMinMax()).to.equal(false) + }) + + it('stepper method checkStatus', () => { + wrapper = mount(Stepper) + wrapper.vm.min = 2 + wrapper.vm.currentNum = 3 + wrapper.vm.step = 2 + wrapper.vm.$_checkStatus() + expect(wrapper.vm.isMin).to.equal(true) + }) + + it('stepper method reset', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = null + wrapper.vm.min = 5 + wrapper.vm.$_reset() + expect(wrapper.vm.currentNum).to.equal(wrapper.vm.min) + }) + + it('stepper method onChange', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = 2 + wrapper.vm.min = 3 + wrapper.vm.$_onChange() + // currentNum < min + expect(wrapper.vm.currentNum).to.equal(wrapper.vm.min) + + wrapper.vm.currentNum = 6 + wrapper.vm.max = 5 + wrapper.vm.$_onChange() + // currentNum > max + expect(wrapper.vm.currentNum).to.equal(wrapper.vm.max) + + wrapper.vm.currentNum = 4 + wrapper.vm.$_onChange() + // min < currentNum < max + expect(wrapper.vm.currentNum).to.equal(4) + }) +}) diff --git a/components/steps/README.md b/components/steps/README.md new file mode 100644 index 00000000..1a0988dd --- /dev/null +++ b/components/steps/README.md @@ -0,0 +1,25 @@ +--- +title: Steps 步骤条 +preview: https://didi.github.io/mand-mobile/examples/steps +--- + +用于引导用户按照流程完成任务的导航条,显示当前所在步骤 + +### 引入 + +```javascript +import { Steps } from 'mand-mobile' + +Vue.component(Steps.name, Steps) +``` + +### 代码演示 + + +### API + +#### Tabs Props +属性 | 说明 | 类型 | 默认值 | 备注 +----|-----|------|------|------ +steps | 步骤信息数组 | Array | - | 数组中每个元素须包含`name`属性,会作为步骤名称显示 +current | 当前步骤 | Number | `0` | 可通过修改该值动态改变当前所在步骤 diff --git a/components/steps/component.js b/components/steps/component.js new file mode 100644 index 00000000..5644b311 --- /dev/null +++ b/components/steps/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'steps', + 'text': '步骤条', + 'category': 'basic', + 'description': '展示当前所在步骤位置的组件。', + 'author': 'zhaozhe' +} diff --git a/components/steps/demo/cases/demo0.vue b/components/steps/demo/cases/demo0.vue new file mode 100644 index 00000000..2b6abfd8 --- /dev/null +++ b/components/steps/demo/cases/demo0.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo1.vue b/components/steps/demo/cases/demo1.vue new file mode 100644 index 00000000..cd3cc4af --- /dev/null +++ b/components/steps/demo/cases/demo1.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo2.vue b/components/steps/demo/cases/demo2.vue new file mode 100644 index 00000000..dcd826bb --- /dev/null +++ b/components/steps/demo/cases/demo2.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo3.vue b/components/steps/demo/cases/demo3.vue new file mode 100644 index 00000000..40709f23 --- /dev/null +++ b/components/steps/demo/cases/demo3.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo4.vue b/components/steps/demo/cases/demo4.vue new file mode 100644 index 00000000..0f8de28b --- /dev/null +++ b/components/steps/demo/cases/demo4.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo5.vue b/components/steps/demo/cases/demo5.vue new file mode 100644 index 00000000..f7ba67b6 --- /dev/null +++ b/components/steps/demo/cases/demo5.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/components/steps/demo/cases/demo6.vue b/components/steps/demo/cases/demo6.vue new file mode 100644 index 00000000..28b51ed3 --- /dev/null +++ b/components/steps/demo/cases/demo6.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/components/steps/demo/index.vue b/components/steps/demo/index.vue new file mode 100644 index 00000000..9c3e649b --- /dev/null +++ b/components/steps/demo/index.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/steps/index.vue b/components/steps/index.vue new file mode 100644 index 00000000..733fef4e --- /dev/null +++ b/components/steps/index.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/components/steps/test/index.spec.js b/components/steps/test/index.spec.js new file mode 100644 index 00000000..e00a3fe6 --- /dev/null +++ b/components/steps/test/index.spec.js @@ -0,0 +1,16 @@ +import Steps from '../index' +import {mount} from 'avoriaz' + +describe('Steps', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple steps', () => { + wrapper = mount(Steps) + + expect(wrapper.hasClass('md-steps')).to.be.true + }) +}) diff --git a/components/swiper-item/index.vue b/components/swiper-item/index.vue new file mode 100644 index 00000000..37d7d7fc --- /dev/null +++ b/components/swiper-item/index.vue @@ -0,0 +1,4 @@ + + diff --git a/components/swiper/README.md b/components/swiper/README.md new file mode 100644 index 00000000..6d155513 --- /dev/null +++ b/components/swiper/README.md @@ -0,0 +1,104 @@ +--- +title: Swiper 轮播 +preview: https://didi.github.io/mand-mobile/examples/swiper +--- + +走马灯,用于一组图片或卡片轮播 + +### 引入 + +```javascript +import { Swiper, SwiperItem } from 'mand-mobile' + +Vue.component(Swiper.name, Swiper) +Vue.component(SwiperItem.name, SwiperItem) +``` + +### 代码演示 + + +### API + +#### Swiper Props + +|属性|说明|类型|默认值|可选值| +|---|---|---|---|---| +|autoplay|自动切换间隔时长(毫秒), 禁用可设置为`0`|Number|`3000`|`0`, `[500, +Int.Max)`| +|transition|面板切换动画效果|String|`slide`|`slide`, `slideY`, `fade`, `fade`| +|default-index|第一屏面板索引值|Number|`0`|`[0, length - 1]`| +|has-dots|控制面板指示点|Boolean|`true`|`true`, `false`| +|is-prevent|是否阻止默认的事件,如页面滚动事件|Boolean|`true`|`true`, `false`| +|is-loop|是否循环播放|Boolean|`true`|`true`, `false`| +|dragable|是否禁用触摸滑动|Boolean|`true`|`true`, `false`| + +#### Swiper Methods + +##### play(autoplay) +打开自动切换 + +|参数|说明|类型|默认值|可选值| +|---|---|---|---|---| +|autoplay|自动切换间隔时长(毫秒)|Number|`3000`|`[500, +Int.Max)`| + +```js +vm.$refs.swiper.play(5000) +``` + +##### stop() +停止自动切换 + +```js +vm.$refs.swiper.stop() +``` + +##### pre() +前一个item + +```js +vm.$refs.swiper.pre() +``` + +##### next() +后一个item + +```js +vm.$refs.swiper.next() +``` + +##### goto(index) +切换到某一个index + +|参数|说明|类型|默认值|可选值| +|---|---|---|---|---| +|index|第一屏面板索引值|Number|`0`|`[0, length - 1]`| +```js +vm.$refs.swiper.goto(2) +``` + +##### getIndex() +获取当前显示的index + +|参数|说明|类型| +|---|---|---| +|index|当前显示的index|Number| + +```js +var index = vm.$refs.swiper.getIndex() +``` + +#### Swiper Events +##### @beforeChange(from, to) +轮播器将要切换前的事件 + +|参数 | 说明 | 类型 | +|----|-----|------| +| from | 轮播器当前展示的索引值 | Number | +| to | 轮播器下一屏展示的索引值 | Number | + +##### @afterChange(from, to) +轮播器切换完成时的事件 + +|参数 | 说明 | 类型 | +|----|-----|------| +| from | 轮播器当前展示的索引值 | Number | +| to | 轮播器下一屏展示的索引值 | Number | diff --git a/components/swiper/component.js b/components/swiper/component.js new file mode 100644 index 00000000..c93d640a --- /dev/null +++ b/components/swiper/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'swiper', + 'text': '轮播', + 'category': 'basic', + 'description': '轮播', + 'author': 'huangbinxing' +} diff --git a/components/swiper/demo/cases/demo0.vue b/components/swiper/demo/cases/demo0.vue new file mode 100644 index 00000000..bbfa23d3 --- /dev/null +++ b/components/swiper/demo/cases/demo0.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/components/swiper/demo/cases/demo1.vue b/components/swiper/demo/cases/demo1.vue new file mode 100644 index 00000000..0ad5af5e --- /dev/null +++ b/components/swiper/demo/cases/demo1.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/components/swiper/demo/cases/demo2.vue b/components/swiper/demo/cases/demo2.vue new file mode 100644 index 00000000..c248d38a --- /dev/null +++ b/components/swiper/demo/cases/demo2.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/components/swiper/demo/cases/demo3.vue b/components/swiper/demo/cases/demo3.vue new file mode 100644 index 00000000..dd6fe20f --- /dev/null +++ b/components/swiper/demo/cases/demo3.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/components/swiper/demo/data/mulit-item.js b/components/swiper/demo/data/mulit-item.js new file mode 100644 index 00000000..9154bfa8 --- /dev/null +++ b/components/swiper/demo/data/mulit-item.js @@ -0,0 +1,82 @@ +var colors = [ + [ + { + color: '#4390EE', + text: '引力波', + }, + { + color: '#CA4040', + text: '智子', + }, + { + color: '#FF8604', + text: '水滴', + }, + { + color: '#00CC00', + text: '二向箔', + }, + { + color: '#0066CC', + text: '飞刃', + }, + { + color: '#99CCCC', + text: '碎星', + }, + ], + [ + { + color: '#990033', + text: '危机', + }, + { + color: '#CCFF66', + text: '威摄', + }, + { + color: '#FF9900', + text: '威摄后', + }, + { + color: '#FF9933', + text: '广播', + }, + { + color: '#99CC33', + text: '掩体', + }, + { + color: '#CC6699', + text: '银河', + }, + ], + [ + { + color: '#0099CC', + text: '猜疑链', + }, + { + color: '#CCCCCC', + text: '技术爆炸', + }, + { + color: '#FF6666', + text: '黑暗森林', + }, + { + color: '#99CCCC', + text: '地球', + }, + { + color: '#FFCC99', + text: '三体', + }, + { + color: '#FFCCCC', + text: '歌者', + }, + ], +] + +export default colors diff --git a/components/swiper/demo/data/simple.js b/components/swiper/demo/data/simple.js new file mode 100644 index 00000000..b81bc9be --- /dev/null +++ b/components/swiper/demo/data/simple.js @@ -0,0 +1,16 @@ +var colors = [ + { + color: '#4390EE', + text: '给时光以生命,给岁月以文明。', + }, + { + color: '#364d79', + text: '你的无畏来源于无知。', + }, + { + color: '#CA4040', + text: '一切都将逝去,只有死神永生。', + }, +] + +export default colors diff --git a/components/swiper/demo/index.vue b/components/swiper/demo/index.vue new file mode 100644 index 00000000..09fa0264 --- /dev/null +++ b/components/swiper/demo/index.vue @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/components/swiper/index.vue b/components/swiper/index.vue new file mode 100644 index 00000000..ca085971 --- /dev/null +++ b/components/swiper/index.vue @@ -0,0 +1,675 @@ + + + + + diff --git a/components/swiper/swiper-item.vue b/components/swiper/swiper-item.vue new file mode 100644 index 00000000..77db5a3e --- /dev/null +++ b/components/swiper/swiper-item.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/components/swiper/test/index.spec.js b/components/swiper/test/index.spec.js new file mode 100644 index 00000000..0f43cbe2 --- /dev/null +++ b/components/swiper/test/index.spec.js @@ -0,0 +1,392 @@ +import Swiper from '../index' +import SwiperItem from '../swiper-item' +import triggerTouch from '../../popup/test/touch-trigger' +import { mount, shallow } from 'avoriaz' + +describe('Swiper', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple swiper', done => { + wrapper = mount(Swiper) + + expect(wrapper.hasClass('md-swiper')).to.be.true + + expect(wrapper.vm.autoplay).to.equal(3000) + expect(wrapper.vm.transition).to.equal('slide') + expect(wrapper.vm.defaultIndex).to.equal(0) + expect(wrapper.vm.hasDots).to.equal(true) + expect(wrapper.vm.isPrevent).to.equal(true) + expect(wrapper.vm.isLoop).to.equal(true) + expect(wrapper.vm.dragable).to.equal(true) + done() + }) + + it('change swiper default props', () => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 5000, + transition: 'slideY', + defaultIndex: 1, + hasDots: false, + isPrevent: false, + isLoop: false, + dragable: false + } + }) + + expect(wrapper.vm.autoplay).to.equal(5000) + expect(wrapper.vm.transition).to.equal('slideY') + expect(wrapper.vm.defaultIndex).to.equal(1) + expect(wrapper.vm.hasDots).to.be.false + expect(wrapper.vm.isPrevent).to.be.false + expect(wrapper.vm.isLoop).to.be.false + expect(wrapper.vm.dragable).to.be.false + + expect(wrapper.vm.isVertical).to.be.true + }) + + // it('set ill props for swiper ', () => { + // wrapper = mount(Swiper, { + // propsData: { + // autoplay: 300, + // transition: 'slideYZ', + // defaultIndex: -1, + // hasDots: 'false', + // isPrevent: 'false', + // isLoop: 'false', + // dragable: 'false' + // } + // }) + // }) + + it('create a swiper item', () => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + expect(wrapper.find('.md-swiper-item')[0].hasStyle('height', 'auto')).to.be.true + }) + + it('create a vertical swiper item', () => { + wrapper = mount(Swiper, { + propsData: { + transition: 'slideY' + }, + slots: { + 'default': SwiperItem + } + }) + expect(wrapper.find('.md-swiper-item')[0].hasStyle('width', 'auto')).to.be.true + }) + + it('swiper method play', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.play(5000) + expect(wrapper.vm.autoplay).to.equal(5000) + done() + }, 1000) + }) + + it('swiper method stop', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.stop() + expect(wrapper.vm.timer).to.equal(null) + done() + }, 1000) + }) + + it('swiper method pre', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + setTimeout(() => { + wrapper.vm.prev() + expect(wrapper.vm.getIndex()).to.equal(2) + done() + }, 500) + }) + + it('swiper method next', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.next() + expect(wrapper.vm.getIndex()).to.equal(1) + done() + }, 500) + }) + + it('swiper method goto', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.goto('a') // ill + expect(wrapper.vm.getIndex()).to.equal(0) + + wrapper.vm.goto(-1) // ill + expect(wrapper.vm.getIndex()).to.equal(0) + + wrapper.vm.goto(3) // ill + expect(wrapper.vm.getIndex()).to.equal(0) + + wrapper.vm.goto(2) + expect(wrapper.vm.getIndex()).to.equal(2) + done() + }, 500) + }) + + it('swiper method getIndex', () => { + wrapper = mount(Swiper) + + expect(wrapper.vm.getIndex()).to.equal(wrapper.vm.index) + }) + + + it('drag swiper', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + // wrapper.vm.$nextTick(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -100, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change', 0, 1)).to.be.true + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(1) + // expect(eventStub.calledWith('after-change', 0, 1)).to.be.true + done() + }, 400) + }, 500) + // }) + }) + + it('drag swiper with a litter distance', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -4, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change')).to.be.false + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 300) + }, 500) + }) + + it('drag swiper with single item', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + isLoop: false + }, + slots: { + 'default': SwiperItem + } + }) + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -100, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change')).to.be.false + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 300) + }, 500) + }) + + it('drag swiper at first item to last item in disloop mode', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + isLoop: false + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', 100, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change')).to.be.false + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 300) + }, 500) + }) + + it('drag swiper at edge (0 -> 2)', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + const hook = wrapper.vm.$el + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', 100, 0) + triggerTouch(hook, 'touchend') + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(2) + done() + }, 1000) + }, 500) + }) + + it('drag swiper at edge (2 -> 0)', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + defaultIndex: 2 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + const hook = wrapper.vm.$el + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -100, 0) + triggerTouch(hook, 'touchend') + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 1000) + }, 500) + }) + + it('drag swiper in disdrag mode', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + transition: 'slideY', + isLoop: false + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + // 初始 index 为 0 + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + + // 初始化后 index 为 0 + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', 0, -50) + triggerTouch(hook, 'touchend') + setTimeout(() => { + // 向右滑动后,index 为 1 + expect(wrapper.vm.getIndex()).to.equal(1) + done() + }, 300) + }, 500) + }) + + it('set transition by fade', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 1000, + transition: 'fade' + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + // 1500 ms 后渐变到第二屏 + expect(wrapper.vm.getIndex()).to.equal(1) + done() + }, 1500) + }) + + // it('swiper destroye', done => { + // wrapper = mount(Swiper, { + // slots: { + // 'default': [SwiperItem, SwiperItem, SwiperItem] + // } + // }) + + // setTimeout(() => { + // // 1500 ms 后渐变到第二屏 + // wrapper.destroy() + // expect(wrapper.vm.timer).to.equal(null) + // expect(wrapper.vm.reInitTimer).to.equal(null) + // done() + // }, 1500) + // }) + + + +}) diff --git a/components/switch/README.md b/components/switch/README.md new file mode 100644 index 00000000..197c1272 --- /dev/null +++ b/components/switch/README.md @@ -0,0 +1,34 @@ +--- +title: Switch 开关 +preview: https://didi.github.io/mand-mobile/examples/switch +--- + +开关按钮,用于表示开关状态/两种状态之间的切换 + +### 引入 + +```javascript +import { Switch } from 'md-mobile' + +Vue.component(Switch.name, Switch) +``` + +### 代码演示 + + +### API + +#### Switch Props +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|v-model|打开或者关闭|Boolean|`false`| +|disabled|是否禁用|Boolean|`false`| + +#### Switch Events + +##### @change(isActive) +事件说明 + +|属性 | 说明 | 类型 | +|----|-----|------| +|isActive|开关状态,打开或者关闭|Boolean| diff --git a/components/switch/component.js b/components/switch/component.js new file mode 100644 index 00000000..00d7452c --- /dev/null +++ b/components/switch/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'switch', + 'text': '滑动开关', + 'category': 'basic', + 'description': '', + 'author': 'chengyanjing' +} diff --git a/components/switch/demo/cases/demo0.vue b/components/switch/demo/cases/demo0.vue new file mode 100644 index 00000000..6af01210 --- /dev/null +++ b/components/switch/demo/cases/demo0.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/cases/demo1.vue b/components/switch/demo/cases/demo1.vue new file mode 100644 index 00000000..f242c055 --- /dev/null +++ b/components/switch/demo/cases/demo1.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/cases/demo2.vue b/components/switch/demo/cases/demo2.vue new file mode 100644 index 00000000..a8f59d9b --- /dev/null +++ b/components/switch/demo/cases/demo2.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/cases/demo3.vue b/components/switch/demo/cases/demo3.vue new file mode 100644 index 00000000..0b7db0b9 --- /dev/null +++ b/components/switch/demo/cases/demo3.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/index.vue b/components/switch/demo/index.vue new file mode 100644 index 00000000..7e595708 --- /dev/null +++ b/components/switch/demo/index.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/components/switch/index.vue b/components/switch/index.vue new file mode 100644 index 00000000..b1eaa633 --- /dev/null +++ b/components/switch/index.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/components/switch/test/index.spec.js b/components/switch/test/index.spec.js new file mode 100644 index 00000000..1df67ccf --- /dev/null +++ b/components/switch/test/index.spec.js @@ -0,0 +1,52 @@ +import Switch from '../index' +import {mount} from 'avoriaz' + +describe('Switch', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple switch', () => { + wrapper = mount(Switch) + expect(wrapper.hasClass('md-switch')).to.be.true + }) + + it('create a simple active switch', () => { + wrapper = mount(Switch, { + propsData: { + value: true, + }, + }) + expect(wrapper.hasClass('active')).to.be.true + + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.trigger('click') + expect(eventStub.calledWith('change')).to.be.true + }) + + it('create a simple inactive switch', done => { + wrapper = mount(Switch, { + propsData: { + value: false, + }, + }) + expect(wrapper.hasClass('active')).to.be.false + + wrapper.vm.value = true + setTimeout(() => { + expect(wrapper.hasClass('active')).to.be.true + done() + }, 300) + }) + + it('create a disabled switch', () => { + wrapper = mount(Switch, { + propsData: { + disabled: true, + }, + }) + expect(wrapper.hasClass('disabled')).to.be.true + }) +}) diff --git a/components/tab-bar/README.md b/components/tab-bar/README.md new file mode 100644 index 00000000..110ef9a0 --- /dev/null +++ b/components/tab-bar/README.md @@ -0,0 +1,47 @@ +--- +title: TabBar 标签栏 +preview: https://didi.github.io/mand-mobile/examples/tab-bar +--- + +用于创建不含内容区域的标签栏 + +### 引入 + +```javascript +import { TabBar } from 'mand-mobile' + +Vue.component(TabBar.name, TabBar) +``` + +### 代码演示 + + +### API + +#### TabBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +| titles | 标签标题数组 | Array | - | 传入该数组会直接根据数组内容渲染组件,也可以不使用该属性,直接在控件中插入定制的标题按钮。在不使用scope-slot时,该值为字符串数组;在使用scope-slot时,该值为对象数组,每个对象会作为props供父组件使用 | +| show-ink-bar | 是否显示下划线 | Boolean | true | - | +| ink-bar-length | 下划线宽度 | Number | 70 | 该数值为下划线占标签按钮宽度的百分比,须在0-100之间 | +| ink-bar-animate | 是否启用下划线动画 | Boolean | true | - | +| default-index | 默认激活的标签索引 | Number | 0 | - | + +#### TabBar Methods + +##### selectTab(index) +选择某一标签 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| index | 标签索引 | Number | - | + +#### TabBar Events + +##### @indexChanged(index, preIndex) +标签索引发生变化 + +|属性 | 说明 | 类型 | +|----|-----|------| +| index | 改变后的标签索引 | Number | +| preIndex | 改变前的标签索引 | Number | diff --git a/components/tab-bar/component.js b/components/tab-bar/component.js new file mode 100644 index 00000000..edbf83d8 --- /dev/null +++ b/components/tab-bar/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tab-bar', + 'text': '标签栏', + 'category': 'basic', + 'description': '用一组标签按钮控制另一区域内容切换的组件。', + 'author': 'zhaozhe' +} diff --git a/components/tab-bar/demo/cases/demo0.vue b/components/tab-bar/demo/cases/demo0.vue new file mode 100644 index 00000000..6778e1a2 --- /dev/null +++ b/components/tab-bar/demo/cases/demo0.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo1.vue b/components/tab-bar/demo/cases/demo1.vue new file mode 100644 index 00000000..2cfcef32 --- /dev/null +++ b/components/tab-bar/demo/cases/demo1.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo2.vue b/components/tab-bar/demo/cases/demo2.vue new file mode 100644 index 00000000..bb9ce248 --- /dev/null +++ b/components/tab-bar/demo/cases/demo2.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo3.vue b/components/tab-bar/demo/cases/demo3.vue new file mode 100644 index 00000000..0aad27f6 --- /dev/null +++ b/components/tab-bar/demo/cases/demo3.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo4.vue b/components/tab-bar/demo/cases/demo4.vue new file mode 100644 index 00000000..1f003567 --- /dev/null +++ b/components/tab-bar/demo/cases/demo4.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo5.vue b/components/tab-bar/demo/cases/demo5.vue new file mode 100644 index 00000000..65bcee54 --- /dev/null +++ b/components/tab-bar/demo/cases/demo5.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo6.vue b/components/tab-bar/demo/cases/demo6.vue new file mode 100644 index 00000000..9d548304 --- /dev/null +++ b/components/tab-bar/demo/cases/demo6.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo7.vue b/components/tab-bar/demo/cases/demo7.vue new file mode 100644 index 00000000..024cdbf0 --- /dev/null +++ b/components/tab-bar/demo/cases/demo7.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo8.vue b/components/tab-bar/demo/cases/demo8.vue new file mode 100644 index 00000000..d627c486 --- /dev/null +++ b/components/tab-bar/demo/cases/demo8.vue @@ -0,0 +1,43 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo9.vue b/components/tab-bar/demo/cases/demo9.vue new file mode 100644 index 00000000..f6bf3ccc --- /dev/null +++ b/components/tab-bar/demo/cases/demo9.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/tab-bar/demo/index.vue b/components/tab-bar/demo/index.vue new file mode 100644 index 00000000..f24b86c9 --- /dev/null +++ b/components/tab-bar/demo/index.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/components/tab-bar/index.vue b/components/tab-bar/index.vue new file mode 100644 index 00000000..df449905 --- /dev/null +++ b/components/tab-bar/index.vue @@ -0,0 +1,180 @@ + + + diff --git a/components/tab-bar/test/index.spec.js b/components/tab-bar/test/index.spec.js new file mode 100644 index 00000000..50679a51 --- /dev/null +++ b/components/tab-bar/test/index.spec.js @@ -0,0 +1,74 @@ +import TabBar from '../index' +import {mount} from 'avoriaz' + +describe('TabBar', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create an empty tab-bar', () => { + wrapper = mount(TabBar) + + expect(wrapper.hasClass('md-tab-bar')).to.be.true + }) + + it('create a tab-bar with title list', () => { + wrapper = mount(TabBar, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(3) + }) + + it('switch index by changing default index', done => { + wrapper = mount(TabBar, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + + wrapper.vm.defaultIndex = 2 + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('switch index by clicking', done => { + wrapper = mount(TabBar, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + wrapper.find('.md-tab-title')[2].trigger('click') + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('create a tab-bar with customized titles', () => { + wrapper = mount(TabBar, { + slots: { + default: [ + { + template: '
title A
', + }, + { + template: '
title B
', + }, + ], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(2) + }) +}) diff --git a/components/tab-picker/README.md b/components/tab-picker/README.md new file mode 100644 index 00000000..7b182e76 --- /dev/null +++ b/components/tab-picker/README.md @@ -0,0 +1,197 @@ +--- +title: TabPicker 多级联动选择器 +preview: https://didi.github.io/mand-mobile/examples/tab-picker +--- + +底部级联选择、非级联选择的tab切换的面板 + +### 引入 + +```javascript +import { TabPicker } from 'mand-mobile' + +Vue.component(TabPicker.name, TabPicker) +``` + +### 使用指南 + +tab切换的title支持自定义渲染(通过slot-scope) +```html + +
+ 标签dom + {{ props.label }} +
+``` +异步级联面板支持传入slot +```html + +
loading内容
+ + +
数据异常
+``` + +### 代码演示 + + +### API + +#### TabPicker Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|控制显示或隐藏|Boolean|`false`| -| +|data|数据源|Array|`[]`|参数据格式考`附录`| +|data-struct|数据级联类型|String|`noCascade`|`normal`, `cascade`, `async`| +|default-index|初始选中项索引|Array|`[]`|-| +|option-render|返回各选项渲染内容|Array|`[]`|`vue 2.1.0+`可使用`slot-scope`,见附录| +|async-func|异步获取数据函数|Function(value, callBack)|-|-| +|title|弹窗标题|Boolean|-|-| +|ok-text|确认按钮文案|String|`确认`|-| +|cancel-text|取消按钮文案|String|`取消`|-| + + +#### TabPicker Methods + +#### getSelectedItem() +获取所有列选中项的值 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{value, lable, ...}>| + +#### TabPicker Events + +#### @change(select) +底部弹窗选中事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|select|各列选中项值|Object: {value,lable}| + +#### @confirm(selected) +底部弹窗确认选择事件 + +|属性 | 说明 | 类型 | +|----|-----|------| +|selected|各列选中项值|Array<{value,lable}>| + +#### @cancel() +底部弹窗取消选择事件 + +#### @show() +底部弹窗弹层展示事件 + +#### @hide() +底部弹窗弹层隐藏事件 + +### 附录 + +* 非级联数据源数据格式 + +```javascript +[ + { + // 选项展示文案 + "label": "", + // 以下自定义字段 + "value": "", + //该选项下的列表 + "children": [ + { + "label": "", + "value": "" + }, + // ... + ] + }, + // ... + // ... +] +``` + +* 级联数据源数据格式 + +```javascript +[ + { + // 选项展示文案 + "label": "", + // 选项值 + "value": "", + // 第二列对应数据 + "children": [ + { + "label": "", + "value": "", + "children": [ + //... + ] + } + ] + }, + //... +] +``` + +* 异步级联数据源数据格式 + +```javascript +{ + "options": [ + { + // 选项展示文案 + "label": "", + // 选项值 + "value": "" + }, + //... + ] + "asyncFunc": (value, callback) => { + callback(null, { + "options": [ + { + "label": '', + "value": '' + }, + //... + ], + "asyncFunc": (value, callback) => { + //... + } + }) + } +} +``` + +* 自定义渲染option + +```html + +``` diff --git a/components/tab-picker/component.js b/components/tab-picker/component.js new file mode 100644 index 00000000..2ddadfc2 --- /dev/null +++ b/components/tab-picker/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tab-picker', + 'text': '底部多级联动选择器', + 'category': 'feedback', + 'description': '', + 'author': 'qiman' +} diff --git a/components/tab-picker/demo/cases/demo0.vue b/components/tab-picker/demo/cases/demo0.vue new file mode 100644 index 00000000..09d3cefd --- /dev/null +++ b/components/tab-picker/demo/cases/demo0.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/components/tab-picker/demo/cases/demo1.vue b/components/tab-picker/demo/cases/demo1.vue new file mode 100644 index 00000000..97bbf500 --- /dev/null +++ b/components/tab-picker/demo/cases/demo1.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/components/tab-picker/demo/cases/demo2.vue b/components/tab-picker/demo/cases/demo2.vue new file mode 100644 index 00000000..6b4e9d8e --- /dev/null +++ b/components/tab-picker/demo/cases/demo2.vue @@ -0,0 +1,108 @@ + + + \ No newline at end of file diff --git a/components/tab-picker/demo/data/cascade.js b/components/tab-picker/demo/data/cascade.js new file mode 100644 index 00000000..84c39917 --- /dev/null +++ b/components/tab-picker/demo/data/cascade.js @@ -0,0 +1,46 @@ +export default [ + { + label: '张三', + value: 1, + children: [ + { + label: '学生', + value: 2, + children: [ + { + label: '男', + value: 11, + children: '', + }, + { + label: '女', + value: 22, + children: '', + }, + ], + }, + ], + }, + { + label: '李四', + value: 2, + children: [ + { + label: '学生', + value: 2, + children: [ + { + label: '男', + value: 11, + children: '', + }, + { + label: '女', + value: 22, + children: '', + }, + ], + }, + ], + }, +] diff --git a/components/tab-picker/demo/data/no-cascade.js b/components/tab-picker/demo/data/no-cascade.js new file mode 100644 index 00000000..84755942 --- /dev/null +++ b/components/tab-picker/demo/data/no-cascade.js @@ -0,0 +1,80 @@ +export default [ + { + label: '第一选择项', + value: '0277', + children: [ + { + label: '武汉', + value: '027', + }, + { + label: '襄阳', + value: '027', + }, + { + label: '十堰', + value: '027', + }, + { + label: '武汉', + value: '027', + }, + { + label: '襄阳', + value: '027', + }, + { + label: '十堰', + value: '027', + }, + { + label: '武汉', + value: '027', + }, + { + label: '襄阳', + value: '027', + }, + { + label: '十堰', + value: '027', + }, + ], + }, + { + label: '第二选择项', + value: '0272', + children: [ + { + label: '成都', + value: '024', + }, + { + label: '汶川', + value: '021', + }, + { + label: '绵阳市', + value: '026', + }, + ], + }, + { + label: '第三选择项', + value: '0247', + children: [ + { + label: '长沙', + value: '0297', + }, + { + label: '株洲', + value: '0273', + }, + { + label: '岳阳', + value: '0207', + }, + ], + }, +] diff --git a/components/tab-picker/demo/index.vue b/components/tab-picker/demo/index.vue new file mode 100644 index 00000000..62661ff8 --- /dev/null +++ b/components/tab-picker/demo/index.vue @@ -0,0 +1,199 @@ + + + + + \ No newline at end of file diff --git a/components/tab-picker/index.vue b/components/tab-picker/index.vue new file mode 100644 index 00000000..55cf3ea0 --- /dev/null +++ b/components/tab-picker/index.vue @@ -0,0 +1,389 @@ + + + + + + + diff --git a/components/tab-picker/test/index.spec.js b/components/tab-picker/test/index.spec.js new file mode 100644 index 00000000..52b650b7 --- /dev/null +++ b/components/tab-picker/test/index.spec.js @@ -0,0 +1,159 @@ +import TabPicker from '../index' +import {mount} from 'avoriaz' +import cascade from '../demo/data/cascade' +import noCascade from '../demo/data/no-cascade' + +describe('TabPicker', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a normal tab-picker', done => { + wrapper = mount(TabPicker, { + propsData: { + data: noCascade, + value: true, + }, + }) + expect(wrapper.vm.data.length).to.equal(3) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(eventStub.calledWith('show')) + expect(wrapper.find('.md-tab-title').length).to.equal(3) + const cancelmBtn = wrapper.find('.md-popup-cancel')[0] + cancelmBtn.trigger('click') + expect(eventStub.calledWith('input')) + expect(eventStub.calledWith('cancel')).to.be.true + setTimeout(() => { + expect(wrapper.vm.isTabPickerShow).to.be.false + expect(eventStub.calledWith('hide')) + done() + }, 500) + }) + }) + + it('create a cascade tab-picker', done => { + wrapper = mount(TabPicker, { + propsData: { + dataStruct: 'cascade', + data: cascade, + value: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(eventStub.calledWith('show')) + expect(wrapper.find('.md-tab-title').length).to.equal(1) + wrapper.find('.md-radio-item')[0].trigger('click') + expect(eventStub.calledWith('change')).to.be.true + setTimeout(() => { + expect(wrapper.vm.renderData.length).to.equal(2) + expect(wrapper.find('.md-tab-title').length).to.equal(2) + wrapper.find('.md-popup-confirm')[0].trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }, 500) + }) + }) + + it('create async tabPicker', done => { + wrapper = mount(TabPicker, { + propsData: { + value: true, + dataStruct: 'async', + asyncFunc: (value, callback) => { + setTimeout(() => { + callback(null, { + options: [ + { + label: '一级选项一', + value: '0271', + }, + { + label: '一级选项二', + value: '0272', + }, + ], + asyncFunc: (value, callback) => { + callback(null, { + options: [ + { + label: '二级选项一', + value: '0271', + }, + { + label: '二级选项二', + value: '0272', + }, + ], + asyncFunc: (value, callback) => { + callback(null, { + options: [ + { + label: '三级选项一', + value: '0271', + }, + { + label: '三级选项二', + value: '0272', + }, + ], + }) + }, + }) + }, + }) + }, 500) + }, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + setTimeout(() => { + expect(wrapper.vm.renderData.length).to.equal(1) + expect(wrapper.find('.md-tab-title').length).to.equal(1) + wrapper.find('.md-radio-item')[0].trigger('click') + expect(eventStub.calledWith('change')).to.be.true + setTimeout(() => { + expect(wrapper.vm.renderData.length).to.equal(2) + expect(wrapper.find('.md-tab-title').length).to.equal(2) + const cancelmBtn = wrapper.find('.md-popup-cancel')[0] + cancelmBtn.trigger('click') + expect(eventStub.calledWith('input')).to.be.false + expect(eventStub.calledWith('cancel')).to.be.true + done() + }, 800) + }, 800) + }) + }) + + it('tabPicker defalut index and casacde', done => { + wrapper = mount(TabPicker, { + propsData: { + data: cascade, + value: true, + dataStruct: 'cascade', + defaultIndex: [0, 0, 0], + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.renderData.length).to.equal(3) + expect(wrapper.find('.md-tab-title').length).to.equal(3) + const confirmBtn = wrapper.find('.md-popup-confirm')[0] + confirmBtn.trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }) + }) +}) diff --git a/components/tabs/README.md b/components/tabs/README.md new file mode 100644 index 00000000..cd8f3ed9 --- /dev/null +++ b/components/tabs/README.md @@ -0,0 +1,48 @@ +--- +title: Tabs 标签页 +preview: https://didi.github.io/mand-mobile/examples/tabs +--- + +用于创建包含内容区域的标签页 + +### 引入 + +```javascript +import { Tabs } from 'mand-mobile' + +Vue.component(Tabs.name, Tabs) +``` + +### 代码演示 + + +### API + +#### Tabs Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|titles|标签标题数组|Array|-|传入该数组会直接根据数组内容渲染组件,也可以不使用该属性,直接在控件中插入定制的标题按钮。在不使用scope-slot时,该值为字符串数组;在使用scope-slot时,该值为对象数组,每个对象会作为props供父组件使用| +|show-ink-bar|是否显示下划线|Boolean|true|-| +|ink-bar-length|下划线宽度|Number|`70`|该数值为下划线占标签按钮宽度的百分比,须在0-100之间| +|ink-bar-animate|是否启用下划线动画|Boolean|`true`|-| +|default-index|默认激活的标签索引|Number|`0`|-| +|noslide|动画样式|Boolean|`false`|如果为真,则不显示滑动动画| + +#### Tabs Methods + +##### selectTab(index) +选择某一标签 + +|属性 | 说明 | 类型 | +|----|-----|------| +|index|标签索引|Number| + +#### Tabs Events + +##### @change(index, preIndex) +标签索引发生变化事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|index|改变后的标签索引|Number| +|preIndex|改变前的标签索引|Number| diff --git a/components/tabs/component.js b/components/tabs/component.js new file mode 100644 index 00000000..4f83596f --- /dev/null +++ b/components/tabs/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tabs', + 'text': '标签页', + 'category': 'basic', + 'description': '用于在内部固定区域切换展示内容的组件。', + 'author': 'zhaozhe' +} diff --git a/components/tabs/demo/cases/demo0.vue b/components/tabs/demo/cases/demo0.vue new file mode 100644 index 00000000..94344bc1 --- /dev/null +++ b/components/tabs/demo/cases/demo0.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo1.vue b/components/tabs/demo/cases/demo1.vue new file mode 100644 index 00000000..5117271e --- /dev/null +++ b/components/tabs/demo/cases/demo1.vue @@ -0,0 +1,30 @@ + + +mand-mobile +import {Tabs} from 'mand-mobile' + +export default { + name: 'tab-bar-demo', + title: '不带下划线', + components: { + [Tabs.name]: Tabs, + }, + data() { + return { + titles: ['第一', '第二', '第三', '第四'], + } + }, +} + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo10.vue b/components/tabs/demo/cases/demo10.vue new file mode 100644 index 00000000..793c57de --- /dev/null +++ b/components/tabs/demo/cases/demo10.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo11.vue b/components/tabs/demo/cases/demo11.vue new file mode 100644 index 00000000..c724e0e7 --- /dev/null +++ b/components/tabs/demo/cases/demo11.vue @@ -0,0 +1,35 @@ + + +mand-mobile +import {Tabs} from 'mand-mobile' + +export default { + name: 'tab-bar-demo', + title: '设置标题栏边距', + components: { + [Tabs.name]: Tabs, + }, + data() { + return { + titles: ['第一', '第二', '第三', '第四'], + } + }, +} + + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo2.vue b/components/tabs/demo/cases/demo2.vue new file mode 100644 index 00000000..ae9d30e5 --- /dev/null +++ b/components/tabs/demo/cases/demo2.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo3.vue b/components/tabs/demo/cases/demo3.vue new file mode 100644 index 00000000..ddb513e1 --- /dev/null +++ b/components/tabs/demo/cases/demo3.vue @@ -0,0 +1,31 @@ + + +mand-mobile +import {Tabs} from 'mand-mobile' + +export default { + name: 'tab-bar-demo', + title: '指定下划线长度', + components: { + [Tabs.name]: Tabs, + }, + data() { + return { + titles: ['第一', '第二', '第三', '第四'], + } + }, +} + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo4.vue b/components/tabs/demo/cases/demo4.vue new file mode 100644 index 00000000..88258143 --- /dev/null +++ b/components/tabs/demo/cases/demo4.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo5.vue b/components/tabs/demo/cases/demo5.vue new file mode 100644 index 00000000..566dcdf2 --- /dev/null +++ b/components/tabs/demo/cases/demo5.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo6.vue b/components/tabs/demo/cases/demo6.vue new file mode 100644 index 00000000..4151aac9 --- /dev/null +++ b/components/tabs/demo/cases/demo6.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo7.vue b/components/tabs/demo/cases/demo7.vue new file mode 100644 index 00000000..8f248620 --- /dev/null +++ b/components/tabs/demo/cases/demo7.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo8.vue b/components/tabs/demo/cases/demo8.vue new file mode 100644 index 00000000..37e0edfa --- /dev/null +++ b/components/tabs/demo/cases/demo8.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo9.vue b/components/tabs/demo/cases/demo9.vue new file mode 100644 index 00000000..17a15260 --- /dev/null +++ b/components/tabs/demo/cases/demo9.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/index.vue b/components/tabs/demo/index.vue new file mode 100644 index 00000000..fa8d7f91 --- /dev/null +++ b/components/tabs/demo/index.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/components/tabs/index.vue b/components/tabs/index.vue new file mode 100644 index 00000000..3b539828 --- /dev/null +++ b/components/tabs/index.vue @@ -0,0 +1,194 @@ + + + diff --git a/components/tabs/test/index.spec.js b/components/tabs/test/index.spec.js new file mode 100644 index 00000000..f0af5fe8 --- /dev/null +++ b/components/tabs/test/index.spec.js @@ -0,0 +1,82 @@ +import Tabs from '../index' +import {mount} from 'avoriaz' + +describe('Tabs', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create an empty tabs', () => { + wrapper = mount(Tabs) + + expect(wrapper.hasClass('md-tabs')).to.be.true + }) + + it('create a tabs with title list', () => { + wrapper = mount(Tabs, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(3) + }) + + it('switch index by using selectTab method', done => { + wrapper = mount(Tabs, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + + wrapper.vm.selectTab(2) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('switch index by clicking', done => { + wrapper = mount(Tabs, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + wrapper.find('.md-tab-title')[2].trigger('click') + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('create a tabs with customized titles and contents', () => { + wrapper = mount(Tabs, { + slots: { + default: [ + { + template: '
content A
', + }, + { + template: '
content B
', + }, + ], + title: [ + { + template: '
title A
', + }, + { + template: '
title B
', + }, + ], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(2) + }) +}) diff --git a/components/tag/README.md b/components/tag/README.md new file mode 100644 index 00000000..bb65da57 --- /dev/null +++ b/components/tag/README.md @@ -0,0 +1,29 @@ +--- +title: Tag 标签 +preview: https://didi.github.io/mand-mobile/examples/tag +--- + +用于表示区域的状态的标签 + +### 引入 + +```javascript +import { Tag } from 'mand-mobile' + +Vue.component(Tag.name, Tag) +``` + +### 代码演示 + + +### API + +#### Tag Props +|属性 | 说明 | 类型 | 默认值 |可选值| +|----|-----|------|------|------| +|size|标签大小|String|`large`|`tiny`, `small`, `large`| +|shape|标签形状|String|`square`|`square`, `circle`, `fillet`| +|type|标签样式|String|`ghost`|`fill`(填充), `ghost`(线框)| +|fill-color|标签颜色`rgba` or `hex number`|String|`rgba(0,0,0,0)`|-| +|font-weight|字体粗细|String|`normal`|`normal`, `bold`, `bolder`| +|font-color|字体颜色`rgba` or `hex number`|String|`#fc9153`|-| diff --git a/components/tag/component.js b/components/tag/component.js new file mode 100644 index 00000000..44bf5613 --- /dev/null +++ b/components/tag/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tag', + 'text': '标签', + 'category': 'basic', + 'description': '小标签', + 'author': 'guoyunlong ' +} diff --git a/components/tag/demo/cases/demo0.vue b/components/tag/demo/cases/demo0.vue new file mode 100644 index 00000000..4e4d3190 --- /dev/null +++ b/components/tag/demo/cases/demo0.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/cases/demo1.vue b/components/tag/demo/cases/demo1.vue new file mode 100644 index 00000000..0a1f2f24 --- /dev/null +++ b/components/tag/demo/cases/demo1.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/cases/demo2.vue b/components/tag/demo/cases/demo2.vue new file mode 100644 index 00000000..b0672f27 --- /dev/null +++ b/components/tag/demo/cases/demo2.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/cases/demo3.vue b/components/tag/demo/cases/demo3.vue new file mode 100644 index 00000000..f0636877 --- /dev/null +++ b/components/tag/demo/cases/demo3.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/index.vue b/components/tag/demo/index.vue new file mode 100644 index 00000000..02da4ded --- /dev/null +++ b/components/tag/demo/index.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/components/tag/index.vue b/components/tag/index.vue new file mode 100644 index 00000000..2255f8df --- /dev/null +++ b/components/tag/index.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/components/tag/test/index.spec.js b/components/tag/test/index.spec.js new file mode 100644 index 00000000..d0fb3b9f --- /dev/null +++ b/components/tag/test/index.spec.js @@ -0,0 +1,44 @@ +import Tag from '../index' +import {mount, shallow} from 'avoriaz' + +describe('Tag', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple tag', () => { + wrapper = mount(Tag) + + expect(wrapper.hasClass('md-tag')).to.be.true + }) + + it('create a circle tag', done => { + wrapper = mount(Tag) + wrapper.setProps({ + shape: 'circle', + fontColor: '#fff', + }) + wrapper.vm.$nextTick(function() { + expect(wrapper.contains('.shape-circle')).to.be.true + done() + }) + }) + it('create a fill tag', () => { + wrapper = mount(Tag) + wrapper.setProps({ + type: 'fill', + fontColor: '#fff', + }) + expect(wrapper.contains('.type-fill')).to.be.true + }) + it('create a ghost tag', () => { + wrapper = mount(Tag) + wrapper.setProps({ + type: 'ghost', + fontColor: '#fff', + }) + expect(wrapper.contains('.type-ghost')).to.be.true + }) +}) diff --git a/components/tip/README.md b/components/tip/README.md new file mode 100644 index 00000000..f66ca23f --- /dev/null +++ b/components/tip/README.md @@ -0,0 +1,29 @@ +--- +title: Tip 气泡提示 +preview: https://didi.github.io/mand-mobile/examples/tip +--- + +### 引入 + +```javascript +import { Tip } from 'mand-mobile' + +Vue.component(Tip.name, Tip) +``` + +### 代码演示 + + +### API + +#### Tips Props +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +|content|提示文本内容|String|-|-| +|placement|位置|String|`top`|`top`, `left`, `bottom`, `right`| + +#### Tip@show() +提示框显示后触发的事件 + +#### Tip@hide() +提示框隐藏后触发的事件 diff --git a/components/tip/component.js b/components/tip/component.js new file mode 100644 index 00000000..f1df301e --- /dev/null +++ b/components/tip/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tip', + 'text': '轻提示', + 'category': 'feedback', + 'description': '弹出式轻提示', + 'author': 'liuxinyumichael' +} diff --git a/components/tip/demo/cases/demo0.vue b/components/tip/demo/cases/demo0.vue new file mode 100644 index 00000000..61296bd4 --- /dev/null +++ b/components/tip/demo/cases/demo0.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/tip/demo/cases/demo1.vue b/components/tip/demo/cases/demo1.vue new file mode 100644 index 00000000..60363553 --- /dev/null +++ b/components/tip/demo/cases/demo1.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/tip/demo/cases/demo2.vue b/components/tip/demo/cases/demo2.vue new file mode 100644 index 00000000..461af31e --- /dev/null +++ b/components/tip/demo/cases/demo2.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/components/tip/demo/cases/demo3.vue b/components/tip/demo/cases/demo3.vue new file mode 100644 index 00000000..81b48bfa --- /dev/null +++ b/components/tip/demo/cases/demo3.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/tip/demo/cases/demo4.vue b/components/tip/demo/cases/demo4.vue new file mode 100644 index 00000000..5f016bb2 --- /dev/null +++ b/components/tip/demo/cases/demo4.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/tip/demo/index.vue b/components/tip/demo/index.vue new file mode 100644 index 00000000..31361f63 --- /dev/null +++ b/components/tip/demo/index.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/components/tip/index.js b/components/tip/index.js new file mode 100644 index 00000000..d1763439 --- /dev/null +++ b/components/tip/index.js @@ -0,0 +1,195 @@ +import Vue from 'vue' +import TipOptions from './tip' +const Tip = Vue.extend(TipOptions) + +export default { + name: 'md-tip', + + props: { + placement: { + type: String, + default: 'top', + }, + content: { + type: String, + default: '', + }, + }, + + mounted() { + this.wrapperEl = this.$_getFirstScrollWrapper(this.$el) + }, + + beforeDestroy() { + if (this.$_tipVM) { + const el = this.$_tipVM.$el + const parent = el.parentNode + if (parent) { + parent.removeChild(el) + } + this.$_tipVM.$destroy() + } + }, + + /** + * Only render the first node of slots + * and add tip tirgger handler on it + */ + render() { + // eslint-disable-line no-unused-vars + if (!this.$slots.default || !this.$slots.default.length) { + return this.$slots.default + } + + const firstNode = this.$slots.default[0] + + const on = (firstNode.data.on = firstNode.data.on || {}) + const nativeOn = (firstNode.data.nativeOn = firstNode.data.nativeOn || {}) + + on.click = this.$_addEventHandle(on.click, this.show) + nativeOn.click = this.$_addEventHandle(nativeOn.click, this.show) + + return firstNode + }, + + methods: { + /** + * Add extra tip trigger handler + * without overwriting the old ones + */ + $_addEventHandle(old, fn) { + if (!old) { + return fn + } else if (Array.isArray(old)) { + return old.indexOf(fn) > -1 ? old : old.concat(fn) + } else { + return old === fn ? old : [old, fn] + } + }, + + /** + * Get the first scrollable parent, + * so we can append the tip element to + * the right parent container + */ + $_getFirstScrollWrapper(node) { + if (node === null || node === document.body) { + return node + } + + const overflowY = window.getComputedStyle(node).overflowY + const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden' + + if (isScrollable && node.scrollHeight > node.clientHeight) { + return node + } else { + return this.$_getFirstScrollWrapper(node.parentNode) + } + }, + + /** + * Get the relative position of an element + * inside a wrapper + */ + $_getPosition(node, wrapper) { + let x = 0 + let y = 0 + let el = node + + while (el) { + x += el.offsetLeft + y += el.offsetTop + + if (el === wrapper || el === document.body || el === null) { + break + } + + el = el.offsetParent + } + + return {x, y} + }, + + /** + * Lazy create tip element + */ + $_getOrNewTip() { + if (this.$_tipVM) { + return this.$_tipVM + } + + const tipVM = (this.$_tipVM = new Tip({ + propsData: { + placement: this.placement, + content: this.content, + }, + }).$mount()) + + tipVM.$on('close', this.hide) + + return tipVM + }, + + /** + * Calculate the position of tip, + * and relayout it's position + */ + layout() { + if (!this.$_tipVM) { + return + } + + const tipElRect = this.$_tipVM.$el.getBoundingClientRect() + const referenceElRect = this.$el.getBoundingClientRect() + const delta = this.$_getPosition(this.$el, this.wrapperEl) + + switch (this.placement) { + case 'left': + delta.y += (referenceElRect.height - tipElRect.height) / 2 + delta.x -= tipElRect.width + 10 + break + + case 'right': + delta.y += (referenceElRect.height - tipElRect.height) / 2 + delta.x += referenceElRect.width + 10 + break + + case 'bottom': + delta.y += referenceElRect.height + 10 + delta.x += (referenceElRect.width - tipElRect.width) / 2 + break + + default: + delta.y -= tipElRect.height + 10 + delta.x += (referenceElRect.width - tipElRect.width) / 2 + break + } + + this.$_tipVM.$el.style.cssText = `position: absolute; top: ${delta.y}px; left: ${delta.x}px;` + }, + + /** + * Do the magic, show me your tip + */ + show() { + const tipVM = this.$_getOrNewTip() + + if (tipVM.$el.parentNode !== this.wrapperEl) { + this.wrapperEl.appendChild(tipVM.$el) + } + + this.layout() + this.$emit('show') + }, + + /** + * Hide tip + */ + hide() { + if (this.$_tipVM && this.$_tipVM.$el.parentNode !== null) { + this.$_tipVM.$el.parentNode.removeChild(this.$_tipVM.$el) + this.$emit('hide') + } + }, + }, +} diff --git a/components/tip/test/index.spec.js b/components/tip/test/index.spec.js new file mode 100644 index 00000000..599a3952 --- /dev/null +++ b/components/tip/test/index.spec.js @@ -0,0 +1,58 @@ +import TipContent from '../tip.vue' +import Button from '../../button/index.vue' +import sinon from 'sinon' +import {mount} from 'avoriaz' + +describe('Tip', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a tip float over left', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + placement: 'left', + }, + }) + + expect(wrapper.hasClass('is-left')).to.be.true + }) + + it('create a tip float over bottom', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + placement: 'bottom', + }, + }) + + expect(wrapper.hasClass('is-bottom')).to.be.true + }) + + it('create a tip float over right', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + placement: 'right', + }, + }) + + expect(wrapper.hasClass('is-right')).to.be.true + }) + + it('click close to hide', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.first('.md-icon-cross').trigger('click') + expect(eventStub.calledWith('close')).to.be.true + }) +}) diff --git a/components/tip/tip.vue b/components/tip/tip.vue new file mode 100644 index 00000000..49941999 --- /dev/null +++ b/components/tip/tip.vue @@ -0,0 +1,96 @@ + + + + + + diff --git a/components/toast/README.md b/components/toast/README.md new file mode 100644 index 00000000..8f180976 --- /dev/null +++ b/components/toast/README.md @@ -0,0 +1,73 @@ +--- +title: Toast 轻提示 +preview: https://didi.github.io/mand-mobile/examples/toast +--- + +弹出式消息提示 + +### 引入 + +```javascript +import { Toast } from 'mand-mobile' + +Toast.succeed('操作成功') +``` + +### 代码演示 + + +### API + +#### Toast({content, icon, duration, hasMask, parentNode}) +显示自定义提示 + +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +| icon | Icon组件图标名称 | String | - |如需自定义图标, 请查看`Icon`组件 | +| content | 提示内容文本 | String | - |- | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | - | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | - | +| parentNode | 组件挂载节点 | HTMLElement | `document.body`|- | + +#### Toast.info(content, duration, hasMask, parentNode) +显示纯文本提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body` | + +#### Toast.succeed(content, duration, hasMask, parentNode) +显示成功提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body` | + +#### Toast.failed(content, duration, hasMask, parentNode) +显示失败提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body`| + +#### Toast.loading(content, duration, hasMask, parentNode) +显示载入提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `0` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `true` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body`| + +#### Toast.hide() +隐藏提示 diff --git a/components/toast/component.js b/components/toast/component.js new file mode 100644 index 00000000..fe20894b --- /dev/null +++ b/components/toast/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'toast', + 'text': '提示', + 'category': 'feedback', + 'description': '弹出式提示', + 'author': 'liuxinyumichael' +} diff --git a/components/toast/demo/cases/demo0.vue b/components/toast/demo/cases/demo0.vue new file mode 100644 index 00000000..e20996a0 --- /dev/null +++ b/components/toast/demo/cases/demo0.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/cases/demo1.vue b/components/toast/demo/cases/demo1.vue new file mode 100644 index 00000000..7c566f85 --- /dev/null +++ b/components/toast/demo/cases/demo1.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/cases/demo2.vue b/components/toast/demo/cases/demo2.vue new file mode 100644 index 00000000..e061db08 --- /dev/null +++ b/components/toast/demo/cases/demo2.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/cases/demo3.vue b/components/toast/demo/cases/demo3.vue new file mode 100644 index 00000000..c21c1b47 --- /dev/null +++ b/components/toast/demo/cases/demo3.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/toast/demo/cases/demo4.vue b/components/toast/demo/cases/demo4.vue new file mode 100644 index 00000000..08d94ceb --- /dev/null +++ b/components/toast/demo/cases/demo4.vue @@ -0,0 +1,27 @@ + + + diff --git a/components/toast/demo/cases/demo5.vue b/components/toast/demo/cases/demo5.vue new file mode 100644 index 00000000..c7244b09 --- /dev/null +++ b/components/toast/demo/cases/demo5.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/index.vue b/components/toast/demo/index.vue new file mode 100644 index 00000000..3edbdde6 --- /dev/null +++ b/components/toast/demo/index.vue @@ -0,0 +1,22 @@ + + + diff --git a/components/toast/index.js b/components/toast/index.js new file mode 100644 index 00000000..40d91e72 --- /dev/null +++ b/components/toast/index.js @@ -0,0 +1,116 @@ +import Vue from 'vue' +import ToastOptions from './toast' +const ToastConstructor = Vue.extend(ToastOptions) + +function Toast({content = '', icon = '', duration = 3000, hasMask = false, parentNode = document.body}) { + let vm = Toast._instance + + if (!vm) { + vm = Toast._instance = new ToastConstructor({ + propsData: { + content, + icon, + duration, + hasMask, + }, + }).$mount() + parentNode.appendChild(vm.$el) + } + + vm.content = content + vm.icon = icon + vm.duration = duration + vm.hasMask = hasMask + vm.visible = true + + return vm +} + +// There is only one toast singleton +Toast._instance = null + +/** + * Hide toast + */ +Toast.hide = () => { + if (Toast._instance instanceof ToastConstructor && Toast._instance.visible) { + Toast._instance.hide() + } +} + +/** + * Show info toast + * @param {string} content + * @param {number=} [duration=3000] + * @param {boolean=} [hasMask=false] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ + +Toast.info = (content = '', duration = 3000, hasMask = false, parentNode = document.body) => { + return Toast({ + icon: '', + content, + duration, + hasMask, + parentNode, + }) +} + +/** + * Show succeed toast + * @param {string} content + * @param {number=} [duration=3000] + * @param {boolean=} [hasMask=false] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ + +Toast.succeed = (content = '', duration = 3000, hasMask = false, parentNode = document.body) => { + return Toast({ + icon: 'circle-right', + content, + duration, + hasMask, + parentNode, + }) +} + +/** + * Show failed toast + * @param {string} content + * @param {number=} [duration=3000] + * @param {boolean=} [hasMask=true] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ + +Toast.failed = (content = '', duration = 3000, hasMask = false, parentNode = document.body) => { + return Toast({ + icon: 'circle-cross', + content, + duration, + hasMask, + parentNode, + }) +} + +/** + * Show loading toast + * @param {string} content + * @param {number=} [duration=0] + * @param {boolean=} [hasMask=false] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ +Toast.loading = (content = '', duration = 0, hasMask = true, parentNode = document.body) => { + return Toast({ + icon: 'spinner', + content, + duration, + hasMask, + parentNode, + }) +} + +export default Toast diff --git a/components/toast/test/index.spec.js b/components/toast/test/index.spec.js new file mode 100644 index 00000000..8cd0291f --- /dev/null +++ b/components/toast/test/index.spec.js @@ -0,0 +1,96 @@ +import Toast from '../toast.vue' +import sinon from 'sinon' +import {mount} from 'avoriaz' + +describe('Toast', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create simple toast', () => { + wrapper = mount(Toast, { + propsData: { + content: 'Hello, Earth!', + }, + }) + + expect(wrapper.hasClass('md-toast')).to.equal(true) + }) + + it('has content', () => { + wrapper = mount(Toast, { + propsData: { + content: 'Hello, Earth!', + }, + }) + + expect( + wrapper + .first('.md-toast-content span') + .text() + .trim(), + ).to.equal('Hello, Earth!') + }) + + it('create icon toast', () => { + wrapper = mount(Toast, { + propsData: { + icon: 'cross', + content: 'Hello, Earth!', + }, + }) + + expect(wrapper.contains('.md-icon')).to.be.true + }) + + it('should update timer after state changed', done => { + wrapper = mount(Toast, { + propsData: { + icon: 'spinner', + content: 'Hello, Earth!', + duration: 1000, + }, + }) + setTimeout(() => { + wrapper.setProps({icon: 'circle-right'}) + setTimeout(function() { + expect(wrapper.vm.visible).to.be.true + done() + }, 500) + }, 800) + }) + + it('auto hide', done => { + wrapper = mount(Toast, { + propsData: { + icon: 'spinner', + content: 'Hello, Earth!', + duration: 1000, + }, + }) + setTimeout(() => { + expect(wrapper.vm.visible).to.be.false + done() + }, 1100) + }) + + // it('emit hide event', done => { + // wrapper = mount(Toast, { + // propsData: { + // icon: 'spinner', + // content: 'Hello, Earth!', + // duration: 0 + // } + // }) + + // const eventStub = sinon.stub(wrapper.vm, '$emit') + // wrapper.vm.hide() + // setTimeout(() => { + // console.log(wrapper.vm.$el) + // expect(eventStub.calledWith('hide')).to.be.true + // done() + // }, 500) + // }) +}) diff --git a/components/toast/toast.vue b/components/toast/toast.vue new file mode 100644 index 00000000..c0416279 --- /dev/null +++ b/components/toast/toast.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/config/dev.env.js b/config/dev.env.js new file mode 100644 index 00000000..1e22973a --- /dev/null +++ b/config/dev.env.js @@ -0,0 +1,7 @@ +'use strict' +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"' +}) diff --git a/config/index.js b/config/index.js new file mode 100644 index 00000000..e1070793 --- /dev/null +++ b/config/index.js @@ -0,0 +1,61 @@ + +'use strict' +// Template version: 1.1.1 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + build: { + env: require('./prod.env'), + assetsRoot: path.resolve(__dirname, '../lib'), + assetsSubDirectory: '', + assetsPublicPath: '/', + productionSourceMap: false, + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + }, + example: { + env: require('./prod.env'), + index: path.resolve(__dirname, '../output/example/index.html'), + assetsRoot: path.resolve(__dirname, '../docs/examples'), + assetsSubDirectory: 'example', + assetsPublicPath: '//static.ins.xiaojukeji.com/static/manhattan/mand-mobile/', + productionSourceMap: false, + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + }, + dev: { + env: require('./dev.env'), + port: process.env.PORT || 4000, + index: 'index.html', + assetsRoot: '/', + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + // CSS Sourcemaps off by default because relative paths are "buggy" + // with this option, according to the CSS-Loader README + // (https://github.com/webpack/css-loader#sourcemaps) + // In our experience, they generally work as expected, + // just be aware of this issue when enabling this option. + cssSourceMap: false + } +} diff --git a/config/prod.env.js b/config/prod.env.js new file mode 100644 index 00000000..a6f99761 --- /dev/null +++ b/config/prod.env.js @@ -0,0 +1,4 @@ +'use strict' +module.exports = { + NODE_ENV: '"production"' +} diff --git a/config/test.env.js b/config/test.env.js new file mode 100644 index 00000000..c2824a30 --- /dev/null +++ b/config/test.env.js @@ -0,0 +1,7 @@ +'use strict' +const merge = require('webpack-merge') +const devEnv = require('./dev.env') + +module.exports = merge(devEnv, { + NODE_ENV: '"testing"' +}) diff --git a/examples/App.vue b/examples/App.vue new file mode 100644 index 00000000..94f1bea5 --- /dev/null +++ b/examples/App.vue @@ -0,0 +1,147 @@ + + + + + + diff --git a/examples/assets/images/bank-zs.svg b/examples/assets/images/bank-zs.svg new file mode 100644 index 00000000..d0cde1cd --- /dev/null +++ b/examples/assets/images/bank-zs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/images/cashier-icon-1.png b/examples/assets/images/cashier-icon-1.png new file mode 100644 index 0000000000000000000000000000000000000000..bee32c2a54176e8859273c3e296151c6f94ee922 GIT binary patch literal 343 zcmV-d0jU0oP)K)p)gaT7hfB*=n*_0FwDmRrTvst+ z;&c=H^D+K2?&msilMq@irPb&Z+)DzyH!QcYg6V=)Jq@UfL8VaATDbrc0&eSQ7SBApFWy<8wur>YJZ_oG+^pBxX z+#FHnayNAT8#FWCp~z-ol*9WQ?9ai$y#F4(Xl5?-oNrL3-}5Ny!rWZAIR(Q9LKqIz z84LTz)QtX(EWAB25PnBUb@m^H;~myvEUdLxzpo$jCr^xC+(%T~EyNSLW}>j#*oz?C pNkY8TE$Pa}JJnfT*4$$7d;t9^9uou&c1i#M002ovPDHLkV1h^%o2~!= literal 0 HcmV?d00001 diff --git a/examples/assets/images/cashier-icon-2.png b/examples/assets/images/cashier-icon-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5a1c9df1fa274a9e5ad43a04b89eb9cc490ac2e0 GIT binary patch literal 549 zcmV+=0^0qFP)_?Zm3iq8J&!~?)&i`HcP8;-h-@Ey3=wK{p#Er7;G8Z@_D zj}hpEQ;+)xNolAXn`d4$*s|bK% zDVb_b7(tRjhT03a^?_nCJSjuo=7|Oco|WD4{VmMMEF@bD^#S^LLVRpiK*U( z0n3NSx&_Ky_yJjJ8eMhj1V3|ezy&nq6n$yS1H;ryGXM2-!PE?tm%2slcQVvAI`hAM z7#Ur>;u%`1Kj5ZbZ6^Ey1KDK#8$pg!iy;2S6$z5U%6Ka+G0`W|y&7I4TQs3yIwSj% zA?`OJ|8 literal 0 HcmV?d00001 diff --git a/examples/assets/images/cashier-icon-3.png b/examples/assets/images/cashier-icon-3.png new file mode 100644 index 0000000000000000000000000000000000000000..fea3fcd0ccbea99ad0062cd59f36808f813eaf36 GIT binary patch literal 451 zcmV;!0X+VRP)71Q{0004sNklg=JFd#ti5E8Ld zs%gdMMuy=N&+x*;EisYU$~*rPYcOF?Ol0;})HOz0g4huYeDU|u{cgj^LI(JzP~DPoBYWM+dmegvTnX+fhb!K2P*+b)Qrf@{)~ t`%Ijsx644#_V-9L)VhG6is)L48GneAV4%yo5oG`X002ovPDHLkV1mQ~&8PqX literal 0 HcmV?d00001 diff --git a/examples/assets/images/cashier-icon-4.png b/examples/assets/images/cashier-icon-4.png new file mode 100644 index 0000000000000000000000000000000000000000..4ccdb19ab8071c629e2dd3991ec781a8310b86f8 GIT binary patch literal 929 zcmV;S177@zP)MUW{a!dgMlR5K>eML^^XAP1zsHCX10Ozo0MWgBcSlW+CU%g+-r}68 z!eU5^Oc>wTf_ZHOl>`laj>ls+MSML&2N*0&H^5J5wyEl|OVJ zHTI-UP+4?5&Kz^YghY`L9ArQZ!LNm3@o91-3i<28-m9(nFfKAa&bOj`_E7N^$s+r{ z%p7x~=y;Mb`t#SBV_;j#C`cPV3^yQSV!@$-7Q7l74Of~Z#Eq8mL-=lNWR?rDqXv~S zVi@dC6&2LDpE3G*(Gcx=`slu#4Cdmbkx@QsSVXo4mf-B*u(;PFCW$qjtPIXMREr)C ztCL5Dy7yR`sGfSG=ke4h(gdDBC&!ukti+K(Qqa*_W|1VUm03-bAS4g2JO3{!lOJyj zrQ%|@__la@bV?i@s#dT!O;oSg{Q2|od);DFq93Z|$G73#;c@aH;OC37Wy|W36i(*rshHf*Q|xGK6m&?R{)H2_a%26})XZGp;Z z)27)3@cZ=xCO}hEg@Y-NgFbZT1Rd1(%SZg2I{{yNx5AfhjZJ0X;o*S@5hB29mEVf( zQ(FcFHJVsW$E2CLi&d*uQBb{}(}da@;?r(v&+g#w-@izgE*-_6+d8QRKHf}|%{GhX z&6|S<%a&Yni8El6j5qk_>1;u<%wfS-%-h==j=)l~XC*R+IPV{7*caa{{WQn)-!nka zzD6pH9ytVjllczwHRErkYp1qoR3ZwvM%xE!i4JU(ww!c8e`d9_pX}VxSGM!BYT-Jm z<)}n;3?4N2nF2w4YH+i`%F4RR9OIm_j(ZMLLlpc6wM<5pU`>Fz00000NkvXXu0mjf D|JB1; literal 0 HcmV?d00001 diff --git a/examples/assets/images/cashier-icon-5.png b/examples/assets/images/cashier-icon-5.png new file mode 100644 index 0000000000000000000000000000000000000000..48908024b61d785c68041f4a59483cc1966ed504 GIT binary patch literal 720 zcmV;>0x$iEP)Zq~im7vHPG0rqe?6A1xjF`t3F8}}k;ed_ns7lFcx4y_3F! z%H@=w^2pBJaDV>#`^+mr)>359J5SkRaocNp`|j|`9W?E;yY|@J@x{ye1RCob&(9>d^0T71a zpYPhVW6?n0xo72M>AlSb73arDxB4uY;Di=?|KZ~&>GPMb7X0@8 zhh;$MV17!!o=Lx@KT`Lv^)@HLf71URnchCBe?Ws6YfDEO8Xl1u9g|*-PfUWf4S*@B zNPSw;4DV;9xp_=rr#$aU3$Q3@36`Z5FupxuRcZrk>)I&8=9ad>c=o<8JJkxn?%qDY z!QnE+APIsIU%=yyGJKJZFPY#=VEB?I;Y5u$I{*Oj-79H`D*hV)0000 window.screen.width) { + window.requestAnimationFrame(resize); + } + else{ + if (ww > 720) { + ww = 720 + } + document.documentElement.style.fontSize = ww * 0.13333333333333333 + 'px'; + document.body.style.opacity = 1; + } + } + + if (document.readyState !== 'loading') { + resize(); + } + else { + document.addEventListener('DOMContentLoaded', resize); + } + + window.addEventListener('resize', resize); + + })(window, document) diff --git a/examples/category.vue b/examples/category.vue new file mode 100644 index 00000000..bf48d15b --- /dev/null +++ b/examples/category.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/examples/components.json b/examples/components.json new file mode 100644 index 00000000..b52cdfcd --- /dev/null +++ b/examples/components.json @@ -0,0 +1 @@ +[{"category":"basic","name":"Basic","text":"基础","list":[{"name":"ActionBar","path":"/action-bar","icon":"action-bar","text":"操作栏"},{"path":"/button","name":"Button","icon":"button","text":"按钮"},{"name":"DropMenu","path":"/drop-menu","icon":"drop-menu","text":"下拉菜单"},{"path":"/icon","name":"Icon","icon":"icon","text":"图标"},{"name":"ImageReader","path":"/image-reader","icon":"image-reader","text":"图片选择器"},{"name":"ImageViewer","path":"/image-viewer","icon":"image-viewer","text":"图片浏览器"},{"name":"NoticeBar","path":"/notice-bar","icon":"notice-bar","text":"通知栏"},{"name":"Stepper","path":"/stepper","icon":"stepper","text":"步进器"},{"name":"Steps","path":"/steps","icon":"steps","text":"步骤条"},{"name":"Swiper","path":"/swiper","icon":"swiper","text":"轮播"},{"name":"TabBar","path":"/tab-bar","icon":"tab-bar","text":"标签栏"},{"name":"Tabs","path":"/tabs","icon":"tabs","text":"标签页"},{"name":"Tag","path":"/tag","icon":"tag","text":"标签"}]},{"category":"business","name":"Business","text":"业务相关","list":[{"name":"Captcha","path":"/captcha","icon":"captcha","text":"验证码窗口"},{"name":"Cashier","path":"/cashier","icon":"cashier","text":"收银台"},{"name":"Chart","path":"/chart","icon":"chart","text":"折线图表"},{"name":"Landscape","path":"/landscape","icon":"landscape","text":"压屏窗"},{"name":"ResultPage","path":"/result-page","icon":"result-page","text":"结果页"}]},{"category":"feedback","name":"Feedback","text":"操作反馈","list":[{"name":"ActionSheet","path":"/action-sheet","icon":"action-sheet","text":"动作面板"},{"name":"DatePicker","path":"/date-picker","icon":"date-picker","text":"日期选择器"},{"name":"Dialog","path":"/dialog","icon":"dialog","text":"模态窗"},{"name":"Picker","path":"/picker","icon":"picker","text":"选择器"},{"name":"Popup","path":"/popup","icon":"popup","text":"弹出层"},{"name":"Selector","path":"/selector","icon":"selector","text":"列表选择器"},{"name":"TabPicker","path":"/tab-picker","icon":"tab-picker","text":"多频道选择器"},{"name":"Tip","path":"/tip","icon":"tip","text":"气泡提示"},{"name":"Toast","path":"/toast","icon":"toast","text":"轻提示"}]},{"category":"form","name":"Form","text":"表单相关","list":[{"name":"Agree","path":"/agree","icon":"agree","text":"勾选按钮"},{"name":"Codebox","path":"/codebox","icon":"codebox","text":"字符码输入框"},{"name":"Field","path":"/field","icon":"field","text":"区域列表组合"},{"name":"InputItem","path":"/input-item","icon":"input-item","text":"输入框"},{"name":"NumberKeyboard","path":"/number-keyboard","icon":"number-keyboard","text":"数字键盘"},{"name":"Radio","path":"/radio","icon":"radio","text":"单选框"},{"name":"Switch","path":"/switch","icon":"switch","text":"开关"}]}] \ No newline at end of file diff --git a/examples/create-demo-module.js b/examples/create-demo-module.js new file mode 100644 index 00000000..c00f1671 --- /dev/null +++ b/examples/create-demo-module.js @@ -0,0 +1,10 @@ +export default function(name = '', demos = []) { + return { + name: `${name}-demo`, + data() { + return { + demos, + } + }, + } +} diff --git a/examples/demo-index.indemand.js b/examples/demo-index.indemand.js new file mode 100644 index 00000000..bc3ad7ca --- /dev/null +++ b/examples/demo-index.indemand.js @@ -0,0 +1,39 @@ +export {default as Home} from './home.indemand.vue' +export {default as Category} from './category' + +export {default as Button} from '../components/button/demo' +export {default as Icon} from '../components/icon/demo' +export {default as Popup} from '../components/popup/demo' +export {default as ActionBar} from '../components/action-bar/demo' +export {default as DropMenu} from '../components/drop-menu/demo' +export {default as Picker} from '../components/picker/demo' +export {default as TabBar} from '../components/tab-bar/demo' +export {default as Swiper} from '../components/swiper/demo' +export {default as Toast} from '../components/toast/demo' +export {default as Dialog} from '../components/dialog/demo' +export {default as Tip} from '../components/tip/demo' +export {default as Tabs} from '../components/tabs/demo' +export {default as Tag} from '../components/tag/demo' +export {default as InputItem} from '../components/input-item/demo' +export {default as NumberKeyboard} from '../components/number-keyboard/demo' +export {default as Stepper} from '../components/stepper/demo' +export {default as Steps} from '../components/steps/demo' +export {default as NoticeBar} from '../components/notice-bar/demo' +export {default as ResultPage} from '../components/result-page/demo' +export {default as ActionSheet} from '../components/action-sheet/demo' +export {default as Selector} from '../components/selector/demo' +export {default as Landscape} from '../components/landscape/demo' +export {default as ImageViewer} from '../components/image-viewer/demo' +export {default as ImageReader} from '../components/image-reader/demo' +export {default as TabPicker} from '../components/tab-picker/demo' +export {default as Field} from '../components/field/demo' +export {default as Switch} from '../components/switch/demo' +export {default as Agree} from '../components/agree/demo' +export {default as Radio} from '../components/radio/demo' +export {default as DatePicker} from '../components/date-picker/demo' +export {default as Captcha} from '../components/captcha/demo' +export {default as Codebox} from '../components/codebox/demo' +export {default as Cashier} from '../components/cashier/demo' +export {default as Chart} from '../components/chart/demo' + +/* @init<%export {default as ${componentNameUpper}} = '../components/${componentName}/demo' */ diff --git a/examples/demo-index.js b/examples/demo-index.js new file mode 100644 index 00000000..6e3caf12 --- /dev/null +++ b/examples/demo-index.js @@ -0,0 +1,38 @@ +export {default as Home} from './home.vue' + +export const Category = r => require.ensure([], () => r(require('./category')), 'category') +export const Button = r => require.ensure([], () => r(require('../components/button/demo')), 'button') +export const Icon = r => require.ensure([], () => r(require('../components/icon/demo')), 'icon') +export const Popup = r => require.ensure([], () => r(require('../components/popup/demo')), 'popup') +export const ActionBar = r => require.ensure([], () => r(require('../components/action-bar/demo')), 'action-bar') +export const DropMenu = r => require.ensure([], () => r(require('../components/drop-menu/demo')), 'drop-menu') +export const Picker = r => require.ensure([], () => r(require('../components/picker/demo')), 'picker') +export const TabBar = r => require.ensure([], () => r(require('../components/tab-bar/demo')), 'tab-bar') +export const Swiper = r => require.ensure([], () => r(require('../components/swiper/demo')), 'swiper') +export const Toast = r => require.ensure([], () => r(require('../components/toast/demo')), 'toast') +export const Dialog = r => require.ensure([], () => r(require('../components/dialog/demo')), 'dialog') +export const Tip = r => require.ensure([], () => r(require('../components/tip/demo')), 'tip') +export const Tabs = r => require.ensure([], () => r(require('../components/tabs/demo')), 'tabs') +export const Tag = r => require.ensure([], () => r(require('../components/tag/demo')), 'tag') +export const InputItem = r => require.ensure([], () => r(require('../components/input-item/demo')), 'input-item') +export const NumberKeyboard = r => + require.ensure([], () => r(require('../components/number-keyboard/demo')), 'number-keyboard') +export const Stepper = r => require.ensure([], () => r(require('../components/stepper/demo')), 'stepper') +export const Steps = r => require.ensure([], () => r(require('../components/steps/demo')), 'steps') +export const NoticeBar = r => require.ensure([], () => r(require('../components/notice-bar/demo')), 'notice-bar') +export const ResultPage = r => require.ensure([], () => r(require('../components/result-page/demo')), 'result-page') +export const ActionSheet = r => require.ensure([], () => r(require('../components/action-sheet/demo')), 'action-sheet') +export const Selector = r => require.ensure([], () => r(require('../components/selector/demo')), 'selector') +export const Landscape = r => require.ensure([], () => r(require('../components/landscape/demo')), 'landscape') +export const ImageViewer = r => require.ensure([], () => r(require('../components/image-viewer/demo')), 'image-viewer') +export const ImageReader = r => require.ensure([], () => r(require('../components/image-reader/demo')), 'image-reader') +export const TabPicker = r => require.ensure([], () => r(require('../components/tab-picker/demo')), 'tab-picker') +export const Field = r => require.ensure([], () => r(require('../components/field/demo')), 'field') +export const Switch = r => require.ensure([], () => r(require('../components/switch/demo')), 'switch') +export const Agree = r => require.ensure([], () => r(require('../components/agree/demo')), 'agree') +export const Radio = r => require.ensure([], () => r(require('../components/radio/demo')), 'radio') +export const DatePicker = r => require.ensure([], () => r(require('../components/date-picker/demo')), 'date-picker') +export const Captcha = r => require.ensure([], () => r(require('../components/captcha/demo')), 'captcha') +export const Codebox = r => require.ensure([], () => r(require('../components/codebox/demo')), 'codebox') +export const Cashier = r => require.ensure([], () => r(require('../components/cashier/demo')), 'cashier') +export const Chart = r => require.ensure([], () => r(require('../components/chart/demo')), 'chart') /* @init<%export const ${componentNameUpper} = r => require.ensure([], () => r(require('../components/${componentName}/demo')), '${componentName}')%> */ diff --git a/examples/home.indemand.vue b/examples/home.indemand.vue new file mode 100644 index 00000000..accfe1a6 --- /dev/null +++ b/examples/home.indemand.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/examples/home.vue b/examples/home.vue new file mode 100644 index 00000000..bce49535 --- /dev/null +++ b/examples/home.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 00000000..1923ce7e --- /dev/null +++ b/examples/index.html @@ -0,0 +1,31 @@ + + + + Mand Mobile + + + + + + + + + + + + +
+ + + + + + + + diff --git a/examples/main.indemand.js b/examples/main.indemand.js new file mode 100644 index 00000000..38ec7d3f --- /dev/null +++ b/examples/main.indemand.js @@ -0,0 +1,32 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import VueRouter from 'vue-router' +import routes from './route.indemand' +import App from './App' + +import '../components/_style/global.styl' +import './theme.custom.styl' + +Vue.config.productionTip = false + +Vue.use(VueRouter) + +const isProd = process.env.NODE_ENV === 'production' + +const router = new VueRouter({ + mode: 'history', + base: isProd ? '/mand-mobile/examples' : '', + routes, +}) + +router.afterEach(route => { + document.title = route.name ? `${route.name}-Mand Mobile` : 'Mand Mobile' +}) + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), + router, +}) diff --git a/examples/main.js b/examples/main.js new file mode 100644 index 00000000..9bc5ae7d --- /dev/null +++ b/examples/main.js @@ -0,0 +1,36 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import VueRouter from 'vue-router' +import routes from './route' +import App from './App' +import FastClick from 'fastclick' +import '../components/_style/global.styl' +import './theme.custom.styl' + +if ('ontouchstart' in window) { + FastClick.attach(document.body) +} + +Vue.config.productionTip = false + +Vue.use(VueRouter) + +const isProd = process.env.NODE_ENV === 'production' + +const router = new VueRouter({ + mode: 'history', + base: isProd ? '/mand-mobile/examples' : '', + routes, +}) + +router.afterEach(route => { + document.title = route.name ? `${route.name}-Mand Mobile` : 'Mand Mobile' +}) + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), + router, +}) diff --git a/examples/route.indemand.js b/examples/route.indemand.js new file mode 100644 index 00000000..6f46da43 --- /dev/null +++ b/examples/route.indemand.js @@ -0,0 +1,46 @@ + + +import components from './components.json' +import * as demo from './demo-index.indemand' + +const traverseComponents = (data, fn) => { + data.map(item => + item.list && item.list.map(subItem => + fn(subItem) + ) + ) +} + +const registerRoute = (components) => { + const routes = [] + traverseComponents(components, (component) => { + routes.push({ + name: component.name, + path: component.path, + // require(`../components${component.path}/demo`).default + component: demo[component.name] || {}, + meta: { + title: component.name || '', + description: component.text || '' + } + }) + }) + return routes +} + +const routes = registerRoute(components) + +routes.push({ + path: '/home', + component: demo['Home'] +}) +routes.push({ + path: '/category', + component: demo['Category'] +}) +routes.push({ + path: '/', + redirect: '/home' +}) + +export default routes diff --git a/examples/route.js b/examples/route.js new file mode 100644 index 00000000..8105b66b --- /dev/null +++ b/examples/route.js @@ -0,0 +1,44 @@ +import components from './components.json' +import * as demo from './demo-index' + +const traverseComponents = (data, fn) => { + data.map(item => + item.list && item.list.map(subItem => + fn(subItem) + ) + ) +} + +const registerRoute = (components) => { + const routes = [] + traverseComponents(components, (component) => { + routes.push({ + name: component.name, + path: component.path, + // require(`../components${component.path}/demo`).default + component: demo[component.name] || {}, + meta: { + title: component.name || '', + description: component.text || '' + } + }) + }) + return routes +} + +const routes = registerRoute(components) + +routes.push({ + path: '/home', + component: demo['Home'] +}) +routes.push({ + path: '/category', + component: demo['Category'] +}) +routes.push({ + path: '/', + redirect: '/home' +}) + +export default routes diff --git a/examples/single-component-app.vue b/examples/single-component-app.vue new file mode 100644 index 00000000..2fcd5485 --- /dev/null +++ b/examples/single-component-app.vue @@ -0,0 +1,126 @@ + + + + + + diff --git a/examples/single-component-main.js b/examples/single-component-main.js new file mode 100644 index 00000000..e16efdab --- /dev/null +++ b/examples/single-component-main.js @@ -0,0 +1,16 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import VueRouter from 'vue-router' +import App from './single-component-app' +import '../components/_style/global.styl' + +Vue.config.productionTip = false + +const isProd = process.env.NODE_ENV === 'production' + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), +}) diff --git a/examples/theme.custom.styl b/examples/theme.custom.styl new file mode 100644 index 00000000..0a9abad3 --- /dev/null +++ b/examples/theme.custom.styl @@ -0,0 +1 @@ +// color-primary = #1AAD19 \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..5850c869 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,172 @@ +'use strict' +const path = require('path') +const gulp = require('gulp') +const gulpBase64 = require('gulp-base64') +const preprocess = require('gulp-preprocess') +const babel = require('gulp-babel') +const gutil = require('gulp-util') +const compiler = require('vueify').compiler +const stylus = require('stylus') +const gulpStylus = require('gulp-stylus') +const through = require('through2') +const merge2 = require('merge2') +const colors = require('colors') +const pkg = require('./package.json') +const components = require('./examples/components.json') +// const execSync = require('child_process').execSync + +colors.setTheme({ + info: 'green', + warn: 'yellow', + error: 'red', + bold: 'bold' +}) + +const componentList = generateComponentsList(components) +const uncompliedComponentList = [...componentList] +const mixins = [ + path.join(__dirname, './components/_style/mixin/*.styl'), + path.join(__dirname, './node_modules/nib/lib/nib/vendor'), + path.join(__dirname, './node_modules/nib/lib/nib/gradients'), + path.join(__dirname, './node_modules/nib/lib/nib/flex') +] + +let succNum = 0 +let failNum = 0 + +function generateComponentsList (components) { + let list = [] + components.map(nav => + nav.list.map(item => + list.push(item.path.substr(1)) + ) + ) + return list +} + +function compilingComponentLog () { + return through.obj((file, encode, callback) => { + if (file.path) { + const res = file.content ? 0 : -1 + file.path.replace(/lib\/((\S)+)/ig, (s, v) => { + if (!v) return + if (res < 0) { + succNum++ + console.log(`${pkg.name}/lib/${v} ✔`.info) + } else { + failNum++ + console.log(`${pkg.name}/lib/${v} ✘ (complie error)`.error) + } + uncompliedComponentList.splice(uncompliedComponentList.indexOf(v), 1) + }) + } + callback() + }) +} + +function compileVueStylus (content, cb, compiler, filePath) { + stylus(content) + .import(mixins[0]) + .import(mixins[1]) + .import(mixins[2]) + .import(mixins[3]) + .render((err, css) => { + if (err) throw err + cb(null, css) + }) +} + +function gulpVueify (options) { + return through.obj(function (file, encode, callback) { + if (file.isNull()) { + return callback(null, file) + } + if (file.isStream()) { + this.emit('error', new gutil.PluginError('gulp-vueify', 'Streams are not supported')) + return callback() + } + if (options) { + compiler.applyConfig(options) + } + const isExactCss = options.isExactCss + let styleContent = '' + const styleCb = res => { + if (res.style) { + styleContent = res.style + } + } + if (isExactCss) { + compiler.on('style', styleCb) + } + compiler.compile(file.contents.toString(), file.path, (err, result) => { + if (err) { + this.emit('error', new gutil.PluginError('gulp-vueify', + `In file ${path.relative(process.cwd(), file.path)}:\n${err.message}`)) + return callback() + } + if (isExactCss) { + // 仅提取css + file.path = gutil.replaceExtension(file.path, '.css') + file.contents = Buffer.from(styleContent) + compiler.removeListener('style', styleCb) + } else { + // js & css 集成至一个文件 + file.path = gutil.replaceExtension(file.path, '.js') + file.contents = Buffer.from(result) + } + callback(null, file) + }) + }) +} + +gulp.task('compile', () => { + const streamCompiledVue = gulp.src('./components/*/*.vue') + .pipe(gulpVueify({ + extractCSS: true, + customCompilers: { + stylus: compileVueStylus + } + })) + .pipe(gulp.dest('./lib')) + .pipe(compilingComponentLog()) + + const streamCompiledCss = gulp.src('./components/*/*.vue') + .pipe(gulpVueify({ + isExactCss: true, + customCompilers: { + stylus: compileVueStylus + } + })) + .pipe(gulpBase64()) + .pipe(gulp.dest('./lib/style')) + + const streamCompiledStylus = gulp.src('./components/_style/*.styl') + .pipe(gulpStylus({ + compress: true, + import: mixins + })) + .pipe(gulp.dest('./lib/style')) + + const streamCompiledJs = gulp.src('./components/**/!(test)/!(component).js') + .pipe(preprocess({ + context: { + 'NODE_ENV': 'production', + 'MAN_VERSION': `'${pkg.version}' //` + }})) + .pipe(babel()) + .pipe(gulp.dest('./lib')) + + return merge2([streamCompiledVue, streamCompiledCss, streamCompiledStylus, streamCompiledJs]) +}) + +gulp.task('build', ['compile'], () => { + uncompliedComponentList.map(item => { + failNum++ + console.log(`${pkg.name}/lib/${item} ✘ (not found)`.error) + }) + console.log( + `\n${pkg.name}(${pkg.version}) `.warn.bold + + `build ${componentList.length} components : `.warn.bold + + `${succNum} successed / ${failNum} failed\n`.warn.bold + ) +}) diff --git a/package.json b/package.json new file mode 100644 index 00000000..d1d1de2e --- /dev/null +++ b/package.json @@ -0,0 +1,212 @@ +{ + "name": "mand-mobile", + "version": "0.5.0-alpha.1", + "description": "A Vue.js 2.0 Mobile UI Toolkit", + "main": "lib/mand-mobile.cjs.js", + "style": "lib/mand-mobile.css", + "module": "lib/mand-mobile.esm.js", + "unpkg": "lib/index.js", + "typings": "types/index.d.ts", + "files": [ + "lib", + "components", + "types" + ], + "scripts": { + "dev:webpack": "webpack-dashboard -m -- node build/webpack/dev-server", + "dev": "node build/rollup/dev-server.rollup", + "dev:site": "cd site && npm start", + "create": "node build/component-init.js", + "cz": "git-cz", + "build": "rm -rf ./lib && npm run build:components && npm run build:mand-mobile", + "build:site": "cd site && npm run generate && npm run build", + "prebuild:example": "npm run build", + "build:example": "cross-env NODE_ENV=production BUILD_TYPE=example node build/rollup/build-example.rollup", + "build:mand-mobile": "cross-env NODE_ENV=production node build/rollup/build-mand-mobile.rollup", + "build:components": "cross-env NODE_ENV=production && node build/rollup/build-component.rollup", + "build:analysis": "cross-env NODE_ENV=production node build/webpack/build-mand-mobile --analysis", + "test:webpack": "cross-env NODE_ENV=testing BABEL_ENV=test karma start test/unit/karma.conf.js", + "test": "cross-env NODE_ENV=testing BABEL_ENV=test karma start test/unit/rollup.karma.conf.js --auto-watch", + "lint": "eslint --ext .js,.vue components test/unit/specs test/e2e/specs", + "precommit": "lint-staged", + "build:webpack": "rm -rf ./lib && npm run build:webpack:mand-mobile && npm run build:webpack:components", + "build:webpack:example": "cross-env NODE_ENV=production node build/webpack/build-example", + "build:webpack:mand-mobile": "cross-env NODE_ENV=production node build/webpack/build-mand-mobile", + "build:webpack:components": "cross-env NODE_ENV=production gulp build --gulpfile gulpfile.js && node build/webpack/build-style-entry" + }, + "license": "Apache", + "config": { + "commitizen": { + "path": "./build/mand-change-log.js" + } + }, + "lint-staged": { + "components/**/*.{vue,js,json}": [ + "aesir lint -s", + "aesir format -S", + "git add" + ] + }, + "devDependencies": { + "@types/babel-generator": "^6.25.1", + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "@types/eslint-plugin-prettier": "^2.2.0", + "@types/glob": "^5.0.35", + "@types/prettier": "^1.8.0", + "@types/vue": "^2.0.0", + "aesir-cli": "^0.0.5", + "autoprefixer": "^7.1.2", + "avoriaz": "^6.0.1", + "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", + "babel-generator": "^6.26.0", + "babel-helper-vue-jsx-merge-props": "^2.0.2", + "babel-loader": "^7.1.1", + "babel-plugin-import": "^1.6.2", + "babel-plugin-istanbul": "^4.1.1", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-preset-env": "^1.6.1", + "babel-preset-es2015": "^6.24.1", + "babel-preset-stage-2": "^6.22.0", + "babel-register": "^6.22.0", + "babel-types": "^6.26.0", + "babelrc-rollup": "^3.0.0", + "babylon": "^6.18.0", + "bluebird": "^3.5.1", + "chai": "^4.1.2", + "chalk": "^2.0.1", + "chromedriver": "^2.27.2", + "commander": "^2.12.2", + "commitizen": "^2.9.6", + "connect-history-api-fallback": "^1.3.0", + "copy-webpack-plugin": "^4.0.1", + "cross-env": "^5.0.1", + "cross-spawn": "^5.0.1", + "css-loader": "^0.28.0", + "cssnano": "^3.10.0", + "dependency-tree": "^5.12.0", + "eslint": "^4.4.1", + "eslint-config-aesir-mandatory": "0.0.3", + "eslint-config-aesir-recommand": "^0.0.2", + "eslint-config-standard": "^10.2.1", + "eslint-friendly-formatter": "^3.0.0", + "eslint-loader": "^1.7.1", + "eslint-plugin-html": "^3.2.0", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-json": "^1.2.0", + "eslint-plugin-node": "^5.1.1", + "eslint-plugin-prettier": "^2.2.0", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^3.0.1", + "eventsource-polyfill": "^0.9.6", + "express": "^4.14.1", + "extract-text-webpack-plugin": "^3.0.0", + "fastclick": "^1.0.6", + "file-loader": "^1.1.4", + "friendly-errors-webpack-plugin": "^1.6.1", + "fs-extra": "^4.0.2", + "git-user-email": "^0.2.2", + "git-user-name": "^2.0.0", + "glob": "^7.1.2", + "gulp": "^3.9.1", + "gulp-babel": "^7.0.0", + "gulp-base64": "^0.1.3", + "gulp-preprocess": "^2.0.0", + "gulp-stylus": "^2.6.0", + "gulp-util": "^3.0.8", + "html-webpack-plugin": "^2.30.1", + "http-proxy-middleware": "^0.17.3", + "husky": "^0.14.3", + "inject-loader": "^3.0.0", + "inquirer": "^3.3.0", + "karma": "^1.4.1", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^1.1.1", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-phantomjs-shim": "^1.4.0", + "karma-rollup-preprocessor": "^5.1.1", + "karma-sinon-chai": "^1.3.1", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.31", + "karma-webpack": "^2.0.2", + "lint-staged": "^5.0.0", + "livereload": "^0.7.0", + "merge2": "^1.2.0", + "mime-types": "^2.1.17", + "mocha": "^3.2.0", + "moment": "^2.19.1", + "needle": "^2.0.1", + "nib": "^1.1.2", + "nightwatch": "^0.9.12", + "opn": "^5.1.0", + "optimize-css-assets-webpack-plugin": "^3.2.0", + "ora": "^1.3.0", + "phantomjs-prebuilt": "^2.1.16", + "portfinder": "^1.0.13", + "postcss-loader": "^2.0.8", + "postcss-pxtorem": "^4.0.1", + "postcss-url": "^7.3.1", + "poststylus": "^1.0.0", + "prettier": "^1.5.3", + "progress-bar-webpack-plugin": "^1.10.0", + "recursive-copy": "^2.0.8", + "rimraf": "^2.6.0", + "rollup": "^0.54.0", + "rollup-plugin-alias": "^1.4.0", + "rollup-plugin-babel": "^3.0.3", + "rollup-plugin-commonjs": "^8.2.6", + "rollup-plugin-css-only": "^0.2.0", + "rollup-plugin-filesize": "^1.5.0", + "rollup-plugin-glob-import": "^0.1.4", + "rollup-plugin-image": "^1.0.2", + "rollup-plugin-import-alias": "^1.0.4", + "rollup-plugin-inject": "^2.0.0", + "rollup-plugin-json": "^2.3.0", + "rollup-plugin-node-builtins": "^2.1.2", + "rollup-plugin-node-globals": "1.1.0", + "rollup-plugin-node-resolve": "^3.0.2", + "rollup-plugin-postcss": "^1.2.9", + "rollup-plugin-progress": "^0.4.0", + "rollup-plugin-replace": "^2.0.0", + "rollup-plugin-require-context": "0.0.2", + "rollup-plugin-stylus-css-modules": "^1.5.0", + "rollup-plugin-template-html": "^0.0.3", + "rollup-plugin-uglify": "^3.0.0", + "rollup-plugin-url": "^1.3.0", + "rollup-plugin-vue": "^3.0.0", + "rollup-pluginutils": "^2.0.1", + "selenium-server": "^3.0.1", + "semver": "^5.4.1", + "shelljs": "^0.7.8", + "sinon": "^4.0.0", + "sinon-chai": "^2.8.0", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "svg-sprite-loader": "^0.3.1", + "url-loader": "^0.5.8", + "vue": "^2.4.2", + "vue-loader": "^13.0.4", + "vue-router": "^3.0.1", + "vue-style-loader": "^3.0.1", + "vue-template-compiler": "^2.4.2", + "vueify": "^9.4.1", + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dashboard": "^1.0.2", + "webpack-dev-middleware": "^1.12.0", + "webpack-hot-middleware": "^2.18.2", + "webpack-merge": "^4.1.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..db061a43 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,10 @@ + +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = (ctx) => ({ + plugins: { + 'postcss-pxtorem': ctx.env !== 'production' ? { rootValue: 100, propWhiteList: [] } : false, + 'postcss-url': {url: 'inline'}, + 'cssnano': { zindex: false, mergeIdents: false, discardUnused: false, autoprefixer: false}, + } +}) diff --git a/site/.babelrc b/site/.babelrc new file mode 100644 index 00000000..0c4cce4b --- /dev/null +++ b/site/.babelrc @@ -0,0 +1,24 @@ +{ + // "presets": [ + // ["env", { + // "modules": false, + // "targets": { + // "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] + // } + // }], + // "stage-2" + // ], + "presets": [ + "env", + "stage-2" + ], + "plugins": [ + ["import", { "libraryName": "mand-mobile", "style": true }] + ], + "env": { + "test": { + "presets": ["env", "stage-2"], + "plugins": ["istanbul"] + } + } +} diff --git a/site/.eslintignore b/site/.eslintignore new file mode 100644 index 00000000..266d451e --- /dev/null +++ b/site/.eslintignore @@ -0,0 +1,3 @@ +build/*.js +config/*.js +public/* diff --git a/site/.eslintrc.js b/site/.eslintrc.js new file mode 100644 index 00000000..e0dd62d9 --- /dev/null +++ b/site/.eslintrc.js @@ -0,0 +1,27 @@ +// https://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true, + }, + // https://github.com/standard/standard/blob/master/docs/RULES-en.md + extends: 'standard', + // required to lint *.vue files + plugins: [ + 'html' + ], + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 00000000..ca370260 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +node_modules/ +dist/ +public/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +test/unit/coverage +test/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/site/.postcssrc.js b/site/.postcssrc.js new file mode 100644 index 00000000..b4d062c1 --- /dev/null +++ b/site/.postcssrc.js @@ -0,0 +1,8 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + "plugins": { + // to edit target browsers: use "browserslist" field in package.json + // "autoprefixer": {} + } +} diff --git a/site/build/bin/default.mfe.blog.config.js b/site/build/bin/default.mfe.blog.config.js new file mode 100644 index 00000000..c2632a2d --- /dev/null +++ b/site/build/bin/default.mfe.blog.config.js @@ -0,0 +1,26 @@ +const path = require('path') + +function resolve(dir) { + return path.join(__dirname, '../..', dir) +} + +module.exports = { + title: '', + subtitle: '', + logo: '', + favicon: '', + theme: 'default', + hasHome: true, + source: [], + markdownBoundary: { + '': '
', + }, + output: resolve('public'), + defaultTemplate: resolve('theme/default/components/Doc.vue'), + links: [], + copyRight: '', + produceBy: '', + powerBy: '', + routePrefix: '', + staticPrefix: '', +} diff --git a/site/build/bin/gen-indices.js b/site/build/bin/gen-indices.js new file mode 100644 index 00000000..e34c5009 --- /dev/null +++ b/site/build/bin/gen-indices.js @@ -0,0 +1,84 @@ +const fs = require('fs') +const path = require('path') +const algoliasearch = require('algoliasearch') +const key = require('./algolia-key') + +const client = algoliasearch('4GDUUWIAWB', key) + +const index = client.initIndex('mand') + +index.clearIndex(err => { + if (err) { + return + } + + generateIndices('../../docs') + + fs.readdir(path.resolve(__dirname, '../../../components'), (err, dirs) => { + if (err) { + console.log(err) + return + } + + dirs.forEach(dir => { + if (dir.indexOf('.') < 0) { + generateIndices(`../../../components/${ dir }`, dir) + } + }) + }) +}) + +function generateIndices(filePath, component) { + fs.readdir(path.resolve(__dirname, filePath), (err, files) => { + if (err) { + console.log(err) + return + } + + let indices = [] + files.forEach(file => { + if (file.indexOf('.md') < 0) { + return + } + + const content = fs.readFileSync(path.resolve(__dirname, `${ filePath }/${ file }`), 'utf8') + + if (content.indexOf('##') < 0) { + return + } + + const matches = content + .replace(/:::[\s\S]*?:::/g, '') + .replace(/```[\s\S]*?```/g, '') + .match(/#{2,5}[^#]*/g) + .map(match => match.replace(/\n+/g, '\n').split('\n').filter(part => !!part)) + .map(match => { + const length = match.length + if (length > 2) { + const desc = match.slice(1, length).join('') + return [match[0], desc] + } + return match + }) + + indices = indices.concat(matches.map(match => { + const isComponent = match[0].indexOf('###') < 0 + const title = match[0].replace(/#{2,5}/, '').trim() + let index + + index = { + component: component || file.replace('.md', ''), + title, + } + + index.ranking = isComponent ? 2 : 1 + index.content = (match[1] || title).replace(/<[^>]+>/g, '') + return index + })) + }) + + index.addObjects(indices, (err, res) => { + console.log(err, res) + }) + }) +} \ No newline at end of file diff --git a/site/build/bin/markdown.js b/site/build/bin/markdown.js new file mode 100644 index 00000000..474cee57 --- /dev/null +++ b/site/build/bin/markdown.js @@ -0,0 +1,49 @@ +'use strict' +const marked = require('marked') +const fm = require('front-matter') +const highlightCore = require('highlight.js') +const renderer = new marked.Renderer() + +let toc = [] // toc html fragment + +/** + * Generate toc during parse heading elements + * @param {string} text content of heading element + * @param {number} level level of heading element + */ +renderer.heading = function(text, level) { + const link = (isShowText = true) => { + return `${isShowText ? text : '#'}` + } + toc.push(link()) + return `${text}${link(false)}` +} + +const highlight = function(code) { + return highlightCore.highlightAuto(code).value +} + +marked.setOptions({ + renderer: renderer, + highlight: highlight, +}) + +/** + * Parse Markdown content to html fragment & get info from markdown + * @param {string} content markdown content + * @return {object} {info, body, toc} + * info {object} markdown info + * body {string} html fragment of markdown content + * toc {string} html fragment of toc + */ +const markdown = function(content) { + const res = fm(content) // get markdown info from front matter + toc = [] + return { + info: res.attributes, + body: JSON.stringify(marked(res.body)), + toc: JSON.stringify(toc.join('')), + } +} + +module.exports = {markdown, highlight} diff --git a/site/build/bin/mfe-blog-dev.js b/site/build/bin/mfe-blog-dev.js new file mode 100644 index 00000000..a8e61d8b --- /dev/null +++ b/site/build/bin/mfe-blog-dev.js @@ -0,0 +1,47 @@ +const path = require('path') +const nodemon = require('nodemon') +const {mbConfig, traverseSource, info} = require('./utils') + +/** + * Get path of files which should be monitored by nodemon + */ +function getWatchPath() { + const watchList = [path.join(__dirname, '../..', 'mfe.blog.config.js')] + + function handlerSingleSource(item) { + const markdownPath = item.markdown + const templatePath = item.template || mbConfig.defaultTemplate + const demoPath = item.demo + + markdownPath && watchList.push(markdownPath) + templatePath && watchList.push(templatePath) + demoPath && watchList.push(demoPath) + } + + traverseSource(mbConfig.source, handlerSingleSource) + + return watchList +} + +/** + * Start Parsing + */ +function startDev() { + nodemon({ + script: path.join(__dirname, 'mfe-blog-generate.js'), + ext: 'js vue md css styl', + stdout: true, + watch: getWatchPath(), + ignore: ['public/*', 'build/*', 'node_modules/*'], + }) + + nodemon + .on('quit', function() { + process.exit() + }) + .on('restart', function(files) { + info(`Parsing ${files}`) + }) +} + +startDev() diff --git a/site/build/bin/mfe-blog-generate.js b/site/build/bin/mfe-blog-generate.js new file mode 100644 index 00000000..18a35c21 --- /dev/null +++ b/site/build/bin/mfe-blog-generate.js @@ -0,0 +1,212 @@ +const fs = require('fs') +const path = require('path') +const mkdirp = require('mkdirp') +const highlight = require('./markdown').highlight +const markdown = require('./markdown').markdown +const {mbConfig, traverseSource, kebabToCamel, info, warn, error} = require('./utils') + +let views, routes + +function resolve(dir) { + return path.join(__dirname, '../../../', dir) +} +/** + * Transform markdown to html fragment && + * Generate entry(index.data.js) of markdown data + * @param {string} markdownPath + * @param {string} outputPath + */ +function generateSourceData(markdownPath, outputPath) { + let markDownContent = '' + + if (fs.existsSync(markdownPath)) { + markDownContent = fs.readFileSync(markdownPath).toString() + markDownContent = parseBoundarySymbolic(markDownContent) + markDownContent = makeJavascriptModule({}, markdown(markDownContent)) + } else { + error(`markdown is not exist : ${markdownPath}`) + } + + fs.writeFileSync(`${outputPath}/index.data.js`, markDownContent) +} + +/** + * Generate entry(index.demo.js) of Demo + * @param {string} markdownPath + * @param {string} outputPath + */ +function generateDemoVue(outputPath, demoPath = []) { + const imports = {} + const exports = [] + + for (let index = 0, len = demoPath.length; index < len; index++) { + const demo = demoPath[index] + const demoName = `demo${index}` + const demoFileName = `${demoName}.vue` + + if (!fs.existsSync(demo)) { + error(`demo is not exist : ${demo}`) + continue + } + + const demoContent = fs.readFileSync(demo).toString() + imports[demoName] = `./${demoFileName}` + exports[index] = `{ component: ${demoName}, code: ${JSON.stringify(highlight(demoContent))} }` + + fs.writeFileSync(`${outputPath}/${demoFileName}`, demoContent) + } + + fs.writeFileSync( `${outputPath}/index.demo.js`, makeJavascriptModule(imports, { demos: `[${exports.join(',')}]`, })) +} + +/** + * Generate entry(index.vue) of Doc + * @param {string} templatePath doc template + * @param {string} outputPath + */ +function generateDocVue(templatePath, outputPath) { + templatePath = templatePath || mbConfig.defaultTemplate + + let template = '' + + if (fs.existsSync(templatePath)) { + template = fs.readFileSync(templatePath).toString() + } else { + error(`template is not exist : ${templatePath}`) + } + + fs.writeFileSync(`${outputPath}/index.vue`, template) +} + +function generateConfig(config) { + const reg = new RegExp(resolve(''), 'ig') + fs.writeFileSync( `${config.output}/config.js`, `window.mbConfig=${JSON.stringify(config)}`.replace(reg, '')) +} + +/** + * Generate routes enttry(route.js) of Doc + * @param {string} outputPath + */ +function generateRoutepath(outputPath) { + fs.writeFileSync( `${outputPath}/route.js`, views.join('') + makeJavascriptModule( {}, { default: `[${routes.join(',')}]`})) +} + +/** + * Generate code for Import Doc vue & Export Doc Route + * @param {object} source + * @param {string} routePath + * @param {array} views + * @param {array} routes + */ +function saveRoutepath(source, routePath, views, routes) { + const name = kebabToCamel(routePath.replace(/\//g, '-')) + const text = source.text + const src = source.src + const entryPath = `${mbConfig.output}${routePath}/index.vue` + const meta = `meta: { text: '${text}', src: '${src || ''}', markdown: '${!!source.markdown || ''}'}` + + let view, route + + if (fs.existsSync(entryPath)) { + view = `const ${name} = r => require.ensure([], () => r(require('.${routePath}')), '${name}');\n` + views.push(view) + route = `{ path: '${routePath}', component: ${name}, ${meta} }` + } else { + if (src && (src.indexOf('//') < 0)) { + route = `{ path: '${routePath}', redirect: '${src}', ${meta} }` + } else { + route = `{ path: '${routePath}', ${meta} }` + } + } + routes.push(route) +} + +/** + * Parse boundary symbolic in marddown content + * @param {string} content + * @return {string} content + */ +function parseBoundarySymbolic(content) { + const boundary = mbConfig.markdownBoundary + for (const key in boundary) { + if (boundary.hasOwnProperty(key)) { + content = content.replace(key, boundary[key]) + } + } + return content +} + +/** + * Generate Js Module + * @param {object} imports + * @param {object} exports + * @return {string} code + */ +function makeJavascriptModule(imports = {}, exports = {}) { + let content = '' + + for (const key in imports) { + if (imports.hasOwnProperty(key)) { + // import [key] from [conent] + content += `import ${key} from ${JSON.stringify(imports[key])};\n` + } + } + + for (const key in exports) { + if (exports.hasOwnProperty(key)) { + const singleExport = typeof exports[key] === 'string' ? exports[key] : JSON.stringify(exports[key]) + // export default [conent] or export const [key] = [conent] + content += key === 'default' ? `export default ${singleExport};\n` : `export const ${key} = ${singleExport};\n` + } + } + + return content +} + +function startGenerate() { + const source = mbConfig.source + const startTmp = Date.now() + + views = [] + routes = [] + + info('Start processing\n') + + if (!source || !source.length) { + warn('No source found!') + end() + return + } + + function end() { + const endTmp = Date.now() + console.log('\n') + info(`Parse completed! Files loaded in ${(endTmp - startTmp) / 1000}s\n`) + } + + function handlerSingleSource(item, path) { + const markdownPath = item.markdown + const templatePath = item.template + const demoPath = item.demo + const outputPath = `${mbConfig.output}/${path.join('/')}` + + mkdirp.sync(outputPath) + + if (markdownPath) { + generateSourceData(markdownPath, outputPath) + generateDemoVue(outputPath, demoPath) + generateDocVue(templatePath, outputPath) + info(`${markdownPath}`) + } + saveRoutepath(item, `/${path.join('/')}`, views, routes) + } + + traverseSource(source, handlerSingleSource) + + generateRoutepath(mbConfig.output) + generateConfig(mbConfig) + end() +} + +// bootstrap +startGenerate() diff --git a/site/build/bin/utils.js b/site/build/bin/utils.js new file mode 100644 index 00000000..a20e07e4 --- /dev/null +++ b/site/build/bin/utils.js @@ -0,0 +1,66 @@ +const colors = require('colors') +const defaultMbConfig = require('./default.mfe.blog.config') + +// Mfe template blog config info +const mbConfig = Object.assign(defaultMbConfig, require('../../mfe.blog.config')) + +colors.setTheme({ + info: 'green', + warn: 'yellow', + error: 'red', + bold: 'bold', +}) + +function log(msg) { + if (process.argv.indexOf('--log') >= 0) { + console.log(msg) + } +} + +function info(msg) { + log('[MTB INFO]'.info.bold + ` ${msg}`) +} + +function warn(msg) { + log('[MTB WARN]'.warn.bold + ` ${msg}`) +} + +function error(msg) { + log('[MTB ERROR]'.error.bold + ` ${msg}`) +} + +// Traverse "source" and do sth with each item +function traverseSource(source, fn, path = [], level = 0) { + for (let i = 0, len = source.length; i < len; i++) { + const item = source[i] + path[level] = item.name + + if (item.menu && Array.isArray(item.menu)) { + const tmpPath = [...path] + level++ + traverseSource(item.menu, fn, path, level) + path = [...tmpPath] + level-- + } + + fn && fn(item, path) + } +} + +// Transform kebab-case to camelCase +function kebabToCamel (str) { + const parts = str.split('-') + let newStr = '' + + for (let i = 0, len = parts.length; i < len; i++) { + const part = parts[i] + if (!part) { + continue + } + newStr += part[0].toLocaleUpperCase() + part.substr(1) + } + + return newStr +} + +module.exports = {mbConfig, traverseSource, kebabToCamel, info, warn, error} diff --git a/site/build/build.js b/site/build/build.js new file mode 100644 index 00000000..4dfd1516 --- /dev/null +++ b/site/build/build.js @@ -0,0 +1,41 @@ +require('./check-versions')() + +const rm = require('rimraf') +const path = require('path') +const chalk = require('chalk') +const webpack = require('webpack') +const config = require('../config') +const webpackConfig = require('./webpack.prod.conf') + +rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { + if (err) { + throw err + } + 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')) + 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" + ) + ) + }) +}) diff --git a/site/build/check-versions.js b/site/build/check-versions.js new file mode 100644 index 00000000..7e211aff --- /dev/null +++ b/site/build/check-versions.js @@ -0,0 +1,48 @@ +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) + } +} diff --git a/site/build/dev-client.js b/site/build/dev-client.js new file mode 100644 index 00000000..66eb6b0c --- /dev/null +++ b/site/build/dev-client.js @@ -0,0 +1,9 @@ +/* eslint-disable */ +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() + } +}) diff --git a/site/build/dev-server.js b/site/build/dev-server.js new file mode 100644 index 00000000..9a3462e2 --- /dev/null +++ b/site/build/dev-server.js @@ -0,0 +1,95 @@ +require('./check-versions')() + +const config = require('../config') +const {info} = require('./bin/utils') + +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') +const 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 devMiddleware = require('webpack-dev-middleware')(compiler, { + publicPath: webpackConfig.output.publicPath, + quiet: true, +}) + +const hotMiddleware = require('webpack-hot-middleware')(compiler, { + log: false, + heartbeat: 2000, +}) + +// 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) + +// 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 + +info('Starting dev server...') +devMiddleware.waitUntilValid(() => { + portfinder.getPort((err, port) => { + if (err) { + _reject(err) + } + process.env.PORT = port + var uri = 'http://localhost:' + port + info('Listening at ' + uri.warn.bold + '\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() + }, +} diff --git a/site/build/utils.js b/site/build/utils.js new file mode 100644 index 00000000..06300518 --- /dev/null +++ b/site/build/utils.js @@ -0,0 +1,83 @@ +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 = [ + resolve('../components/_style/mixin/theme.styl'), + resolve('../components/_style/mixin/util.styl'), + resolve('theme/default/assets/css/mixin.styl') + ] + + return { + css: generateLoaders('postcss'), + postcss: generateLoaders('postcss'), + less: generateLoaders('less'), + sass: generateLoaders('sass', {indentedSyntax: true}), + scss: generateLoaders('sass'), + 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 +} diff --git a/site/build/vue-loader.conf.js b/site/build/vue-loader.conf.js new file mode 100644 index 00000000..20cf2a6d --- /dev/null +++ b/site/build/vue-loader.conf.js @@ -0,0 +1,16 @@ +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', + }, +} diff --git a/site/build/webpack.base.conf.js b/site/build/webpack.base.conf.js new file mode 100644 index 00000000..417c98c7 --- /dev/null +++ b/site/build/webpack.base.conf.js @@ -0,0 +1,71 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const config = require('../config') +const vueLoaderConfig = require('./vue-loader.conf') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { mbConfig } = require('./bin/utils') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} + +module.exports = { + entry: { + app: [`${mbConfig.output}/config.js`, `./theme/${mbConfig.theme}/main.js`] + }, + output: { + path: config.build.assetsRoot, + filename: '[name].js', + chunkFilename: '[name].js', + publicPath: process.env.NODE_ENV === 'production' + ? config.build.assetsPublicPath + : process.env.NODE_ENV === 'testing' + ? '/mand-mobile/' + : config.dev.assetsPublicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + 'vue$': 'vue/dist/vue.esm.js', + '@examples': resolve('../examples'), + } + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader?cacheDirectory', + include: [resolve('theme'), mbConfig.output] + }, + { + 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('../examples/assets/images')] + } + ] + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: `index.html`, + title: mbConfig.title, + subtitle: mbConfig.subtitle, + logo: mbConfig.favicon, + chunksSortMode: 'dependency' + }) + ] +} diff --git a/site/build/webpack.dev.conf.js b/site/build/webpack.dev.conf.js new file mode 100644 index 00000000..bf10b210 --- /dev/null +++ b/site/build/webpack.dev.conf.js @@ -0,0 +1,40 @@ +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 HtmlWebpackPlugin = require('html-webpack-plugin') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') + +const pxtoremConfig = pxtorem({ rootValue: 100, propWhiteList: [] }) +// 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]) +}) + +module.exports = merge(baseWebpackConfig, { + 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 webpack.DefinePlugin({ + 'process.env': config.dev.env, + }), + // 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(), + new FriendlyErrorsPlugin(), + ], +}) diff --git a/site/build/webpack.prod.conf.js b/site/build/webpack.prod.conf.js new file mode 100644 index 00000000..bdbfa970 --- /dev/null +++ b/site/build/webpack.prod.conf.js @@ -0,0 +1,73 @@ +const path = require('path') +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const baseWebpackConfig = require('./webpack.base.conf') +const ProgressBarPlugin = require('progress-bar-webpack-plugin') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') +const MinifyPlugin = require("babel-minify-webpack-plugin") + +const env = config.build.env + +const webpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ + sourceMap: config.build.productionSourceMap, + extract: true, + }), + }, + devtool: false, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash:8].js'), + chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js'), + }, + plugins: [ + new ProgressBarPlugin({ + format: ' BUILD SITE [:bar] :percent (:elapsed seconds)', + clear: false + }), + // http://vuejs.github.io/vue-loader/en/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env, + }), + new MinifyPlugin({}, { + sourceMap: true, + }), + // extract css into its own file + new ExtractTextPlugin({ + filename: utils.assetsPath('css/[name].[contenthash:8].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, + }, + }), + // 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'], + }), + ], +}) + +module.exports = webpackConfig diff --git a/site/config/dev.env.js b/site/config/dev.env.js new file mode 100644 index 00000000..2772aa32 --- /dev/null +++ b/site/config/dev.env.js @@ -0,0 +1,6 @@ +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"', +}) diff --git a/site/config/index.js b/site/config/index.js new file mode 100644 index 00000000..ef0b2bcc --- /dev/null +++ b/site/config/index.js @@ -0,0 +1,41 @@ +const {mbConfig} = require('../build/bin/utils') +// Template version: 1.1.3 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + build: { + env: require('./prod.env'), + index: path.resolve(__dirname, '../dist/index.html'), + assetsRoot: path.resolve(__dirname, '../../docs'), + assetsSubDirectory: 'static', + assetsPublicPath: mbConfig.staticPrefix ? mbConfig.staticPrefix : '/', + productionSourceMap: true, + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report, + }, + dev: { + env: require('./dev.env'), + port: process.env.PORT || 8080, + autoOpenBrowser: true, + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + // CSS Sourcemaps off by default because relative paths are "buggy" + // with this option, according to the CSS-Loader README + // (https://github.com/webpack/css-loader#sourcemaps) + // In our experience, they generally work as expected, + // just be aware of this issue when enabling this option. + cssSourceMap: false, + }, +} diff --git a/site/config/prod.env.js b/site/config/prod.env.js new file mode 100644 index 00000000..1d1c74fd --- /dev/null +++ b/site/config/prod.env.js @@ -0,0 +1,3 @@ +module.exports = { + NODE_ENV: '"production"', +} diff --git a/site/index.html b/site/index.html new file mode 100644 index 00000000..f4e77743 --- /dev/null +++ b/site/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + +
+ + + + + + + + diff --git a/site/mfe.blog.config.js b/site/mfe.blog.config.js new file mode 100644 index 00000000..5cbc0812 --- /dev/null +++ b/site/mfe.blog.config.js @@ -0,0 +1,153 @@ +const fs = require('fs') +const path = require('path') +const components = require('../examples/components.json') + +function resolve(dir) { + return path.join(__dirname, '..', dir) +} + +function generateDemos (name) { + const demoPath = resolve(`components/${name}/demo/cases`) + + if (fs.existsSync(demoPath)) { + const files = fs.readdirSync(demoPath) + return files.map(file => { + return `${demoPath}/${file}` + }) + } else { + return [] + } +} + +function generateSource () { + const menus = [] + components.forEach(category => { + const list = category.list + const subMenus = [] + + list && list.forEach(component => { + subMenus.push({ + name: component.path.substr(1), + text: `${component.name} ${component.text}`, + markdown: resolve(`components${component.path}/README.md`), + demo: generateDemos(component.path.substr(1)) + }) + }) + menus.push({ + name: category.category, + text: category.name, + menu: subMenus + }) + }) + + return menus +} + +module.exports = { + title: 'Mand Mobile', + subtitle: 'Manhattan Design Mobile', + logo: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-logo-black.svg', + favicon: '//static.galileo.xiaojukeji.com/static/tms/other/mand-mobile-logo.png', + source: [ + { + name: 'design', + text: '设计', + src: '/design/resource', + menu: [ + { + name: 'resource', + text: '设计资源', + markdown: resolve('site/docs/resource.md'), + } + ] + }, + { + name: 'docs', + text: '组件', + src: '/docs/introduce', + menu: [ + { + name: 'introduce', + text: 'Mand Mobile', + markdown: resolve('site/docs/introduce.md'), + }, + { + name: 'started', + text: '快速上手', + markdown: resolve('site/docs/started.md'), + }, + { + name: 'development', + text: '开发指南', + markdown: resolve('site/docs/development.md'), + }, + { + name: 'changelog', + text: '更新日志', + markdown: resolve('CHANGELOG.md'), + }, + { + name: 'theme', + text: '主题定制', + markdown: resolve('site/docs/theme.md'), + }, + { + name: 'preview', + text: '组件概览', + template: resolve('site/theme/default/Preview.vue'), + markdown: resolve('README.md'), + }, + { + name: 'components', + text: 'Components', + menu: generateSource() + }, + ], + }, + ], + components: generateSource(), + markdownBoundary: { + '': '
', + '': + '', + }, + links: [ + { + title: '链接', + link: [ + { + text: 'GitHub', + src: 'https://github.com/didi/mand-mobile', + }, + { + text: '更新日志', + src: '//didi.github.io/mand-mobile/docs/changelog', + }, + { + text: '贡献指南', + src: 'https://github.com/didi/mand-mobile/blob/master/CONTRIBUTING.md', + }, + { + text: '问题反馈', + src: 'https://github.com/didi/mand-mobile/issues', + } + ], + }, + { + title: '更多产品', + link: [ + { + text: 'cube-ui - Vue.js组件库', + src: 'https://didi.github.io/cube-ui', + }, + { + text: 'VirtualAPK - Android 插件化框架', + src: 'https://didi.github.io/virtual-apk.html', + } + ], + }, + ], + copyRight: '2012-2018 Didi Chuxing. All Rights Reserved', + routePrefix: '/mfe/mand-mobile', + // staticPrefix: '//manhattan.didistatic.com/static/manhattan/mand-mobile', +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 00000000..5ff13f52 --- /dev/null +++ b/site/package.json @@ -0,0 +1,90 @@ +{ + "name": "mand-mobile-site", + "version": "1.0.0", + "description": "Document Site For Mand Mobile", + "author": "xuxiaoyan ", + "private": true, + "scripts": { + "dev": "node ./build/dev-server --log", + "generate": "node ./build/bin/mfe-blog-generate --log", + "start": "node ./build/bin/mfe-blog-dev --log & npm run dev", + "build:test": "cross-env NODE_ENV=testing node build/build", + "build": "cross-env NODE_ENV=production node build/build", + "indices": "node ./build/bin/gen-indices --log" + }, + "dependencies": { + "vue": "^2.5.2", + "vue-qrcode-component": "^2.1.1", + "vue-router": "^3.0.1" + }, + "devDependencies": { + "algoliasearch": "^3.25.1", + "autoprefixer": "^7.1.2", + "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", + "babel-loader": "^7.1.1", + "babel-minify-webpack-plugin": "^0.3.0", + "babel-plugin-istanbul": "^4.1.1", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-preset-env": "^1.3.2", + "babel-preset-stage-2": "^6.22.0", + "babel-register": "^6.22.0", + "colors": "^1.1.2", + "connect-history-api-fallback": "^1.3.0", + "copy-webpack-plugin": "^4.0.1", + "cross-env": "^5.0.1", + "cross-spawn": "^5.0.1", + "css-loader": "^0.28.0", + "eslint": "^3.19.0", + "eslint-config-standard": "^10.2.1", + "eslint-friendly-formatter": "^3.0.0", + "eslint-loader": "^1.7.1", + "eslint-plugin-html": "^3.0.0", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-node": "^5.2.0", + "eslint-plugin-promise": "^3.4.0", + "eslint-plugin-standard": "^3.0.1", + "eventsource-polyfill": "^0.9.6", + "express": "^4.14.1", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^1.1.4", + "friendly-errors-webpack-plugin": "^1.6.1", + "front-matter": "^2.3.0", + "highlight.js": "^9.12.0", + "html-webpack-plugin": "^2.30.1", + "http-proxy-middleware": "^0.17.3", + "inject-loader": "^3.0.0", + "mand-mobile": "^0.4.21", + "marked": "^0.3.6", + "nightwatch": "^0.9.12", + "nodemon": "^1.12.1", + "opn": "^5.1.0", + "optimize-css-assets-webpack-plugin": "^3.2.0", + "ora": "^1.2.0", + "portfinder": "^1.0.13", + "rimraf": "^2.6.0", + "selenium-server": "^3.0.1", + "semver": "^5.3.0", + "shelljs": "^0.7.6", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "url-loader": "^0.5.8", + "vue-loader": "^13.3.0", + "vue-style-loader": "^3.0.1", + "vue-template-compiler": "^2.5.2", + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dev-middleware": "^1.12.0", + "webpack-hot-middleware": "^2.18.2", + "webpack-merge": "^4.1.0" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/site/theme/default/App.vue b/site/theme/default/App.vue new file mode 100644 index 00000000..1bd30854 --- /dev/null +++ b/site/theme/default/App.vue @@ -0,0 +1,87 @@ + + + + + + diff --git a/site/theme/default/Error.vue b/site/theme/default/Error.vue new file mode 100644 index 00000000..14796cf5 --- /dev/null +++ b/site/theme/default/Error.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/site/theme/default/Home.vue b/site/theme/default/Home.vue new file mode 100644 index 00000000..b900f9ef --- /dev/null +++ b/site/theme/default/Home.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/site/theme/default/Preview.vue b/site/theme/default/Preview.vue new file mode 100644 index 00000000..56f5f6c7 --- /dev/null +++ b/site/theme/default/Preview.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/site/theme/default/assets/css/demo.styl b/site/theme/default/assets/css/demo.styl new file mode 100644 index 00000000..122536e3 --- /dev/null +++ b/site/theme/default/assets/css/demo.styl @@ -0,0 +1,158 @@ +.md-example-child + box-sizing border-box + +.md-example-child-button, .md-example-child-landscape, .md-example-child-action-sheet, .md-example-child-date-picker + , .md-example-child-icon, .md-example-child-reader, .md-example-child-notice-bar, .md-example-child-number-keyboard + , .md-example-child-picker, .md-example-child-popup, .md-example-child-result-page, .md-example-child-tab-picker + , .md-example-child-toast, .md-example-child-dialog, .md-example-child-cashier, .md-example-chart-child + clearfix() + padding 20px + +.md-example-child-button + .md-button + float left + margin-bottom 10px +.md-example-child-agree + margin-top 70px + padding 0 h-gap-lg + font-size font-minor-large +.md-example-child-drop-menu + position absolute + width 100% + height 100% + .md-drop-menu-bar + height drop-menu-height !important +.md-example-child-field + .md-input-item + background #FFF + padding 0 32px + .strong-tip + font-size 24px + color color-text-hightlight +.md-example-child-icon + &.md-example-child-icon-1 + display flex + align-items center + height 200px + .md-example-item, .md-example-item-s + float left + width 33% + padding 15px 0 + color #333 + text-align center + p + text-align center + font-size font-body-normal + color #666 + .md-example-item-s + width 25% +.md-example-child-popup + .md-popup.with-mask, .md-popup-box + position absolute !important +.md-example-child-radio + .md-field-item-custom-title + font-weight font-weight-medium + .md-field-item-custom-brief + font-size font-minor-normal +.md-example-child-selector + .md-popup.with-mask + position absolute !important +.md-example-child-steps + position relative + top 40px +.md-example-child-switch + display flex + align-items center + justify-content center + height 194px +.md-example-child-tabs + .md-tab-content + background #f + height 100px + line-height 100px + text-align center +.md-example-child-tag + display flex + align-items center + justify-content center + height 194px + .md-tag + margin-right h-gap-sm +.md-example-child-tab-picker + .md-popup.with-mask + position absolute !important +.md-example-child-dialog + .md-button + margin-bottom 20px +.md-example-child-tip + zoom 1 !important + display flex + align-items center + justify-content center + height 194px +.md-example-child-stepper + display flex + align-items center + height 194px + .md-field + flex 1 +.md-example-child-notice-bar + display flex + align-items center + height 194px + .md-notice-bar + flex 1 +.md-example-child-codebox + display flex + align-items center + justify-content center + height 194px +.md-example-chart-child + svg + width 100% !important + +.md-image-viewer + position absolute !important +.md-action-bar + position absolute !important +.md-action-sheet + .md-popup + position absolute !important +.md-date-picker + .md-popup + position absolute !important +.md-drop-menu + position absolute !important + height 100% !important + z-index 100 + .md-drop-menu-bar + height drop-menu-height + .md-popup + position absolute !important +.md-dialog .md-popup-box, .md-toast + zoom .6 +.md-number-keyboard + .md-popup-box + left 50% !important + max-width 800px !important + margin-left -400px !important + z-index 1104 !important + box-shadow 0 4px 20px rgba(0, 0, 0, .1) +.md-captcha + z-index 1402 !important + .md-number-keyboard .md-popup-box + position fixed !important + zoom .6 + .md-dialog .md-popup-box + top 30% !important +.md-landscape + .md-popup.with-mask, .md-popup-box, .close + position absolute !important + .close + bottom 5% !important +.md-picker + .md-popup + position absolute !important +.md-cashier + .md-popup.with-mask + position absolute !important diff --git a/site/theme/default/assets/css/global.styl b/site/theme/default/assets/css/global.styl new file mode 100644 index 00000000..9e898545 --- /dev/null +++ b/site/theme/default/assets/css/global.styl @@ -0,0 +1,17 @@ +.doc-demo-title a + margin-right 5px + background color-primary-tap + color #fff + padding 5px 10px + border-radius 4px + font-size 16px + text-decoration none +.doc-demo-box-priview ul>li + list-style none !important + +.doc-content-qrcode + img + display inline-block + width 100% + margin-top 10px + opacity .8 \ No newline at end of file diff --git a/site/theme/default/assets/css/hightlight.css b/site/theme/default/assets/css/hightlight.css new file mode 100644 index 00000000..35ca8f87 --- /dev/null +++ b/site/theme/default/assets/css/hightlight.css @@ -0,0 +1,17 @@ +.hljs{display:block;overflow-x:auto;padding:.5em;background:#f8f8f8;color:#333} +.hljs-comment,.hljs-quote{color:#999;font-style:italic} +.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:500} +.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal} +.hljs-doctag,.hljs-string{color:#d14} +.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:500} +.hljs-subst{font-weight:400} +.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:500} +.hljs-attribute,.hljs-name,.hljs-tag{color:#256ea2;font-weight:400} +.hljs-link,.hljs-regexp{color:#009926} +.hljs-bullet,.hljs-symbol{color:#990073} +.hljs-built_in,.hljs-builtin-name{color:#0086b3} +.hljs-meta{color:#999;font-weight:500} +.hljs-deletion{background:#fdd} +.hljs-addition{background:#dfd} +.hljs-emphasis{font-style:italic} +.hljs-strong{font-weight:500} diff --git a/site/theme/default/assets/css/markdown.styl b/site/theme/default/assets/css/markdown.styl new file mode 100644 index 00000000..a865df01 --- /dev/null +++ b/site/theme/default/assets/css/markdown.styl @@ -0,0 +1,451 @@ +.doc-content-paragraph, .doc-demo-box-code{ + * { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace + } + h1, h2, h3, h4 { + color: #333; + font-weight: 500; + margin-top: 1.5em; + } + + h1, h2, h3, h4, h5, p , dl{ + margin-bottom: 16px; + padding: 0; + line-height: 1.2; + -webkit-font-smoothing: antialiased; + } + h1, h2, h3, h4, h5{ + a{ + margin-left: 10px; + display: none; + text-decoration: none; + font-weight: bold; + } + &:hover a{ + display: inline; + } + } + h1 { + font-size: 32px; + } + h2 { + font-size: 28px; + } + h1, h2 { + padding-bottom: 10px; + } + h3 { + font-size: 22px; + } + h4 { + font-size: 18px; + } + h5 { + font-size: 16px; + font-weight: 400; + margin-top: 1em; + } + a { + color: #3ca0e6; + margin: 0; + padding: 0; + vertical-align: baseline; + margin: 0 5px; + text-decoration: none; + } + a:hover { + text-decoration: none; + color: #ff6600; + } + a:visited { + /*color: purple;*/ + } + ul, ol { + padding: 0; + padding-left: 24px; + margin-top: 10px; + } + li { + margin-bottom: 5px; + line-height: 24px; + list-style: circle; + } + p, ul, ol { + font-size: 14px; + line-height: 24px; + } + + ol ol, ul ol { + list-style-type: lower-roman; + } + + p { + color: #5e6d82; + } + + code, pre { + border-radius: 3px; + color: inherit; + } + + code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important; + // -webkit-font-smoothing: antialiased; + margin: 0 2px; + padding: 0 5px; + border: solid 1px #f0f0f0; + background-color:#f2f4f5; + } + + code * { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important; + font-size: 14px; + } + + pre { + position: relative; + margin-bottom: 1em; + line-height: 1.7em; + overflow: auto; + padding: 15px 30px; + box-sizing: border-box; + background-color:#f2f4f5; + } + + pre > code { + border: 0; + display: block; + max-width: initial; + padding: 0; + margin: 0; + overflow: auto; + line-height: inherit; + font-size: 14px; + white-space: pre; + background: 0 0; + } + + code { + color: #666555; + } + code:after{ + content: ''; + position: absolute; + top: 5px; + right: 5px; + color: #ddd; + font-size: 12px; + } + code.lang-vue:after{ + content: 'Vue'; + } + code.lang-css:after{ + content: 'Css'; + } + code.lang-javascript:after{ + content: 'Js'; + } + code.lang-shell:after{ + content: 'Shell'; + } + code.lang-stylus:after{ + content: 'Stylus'; + } + + /** markdown preview plus 对于代码块的处理有些问题, 所以使用统一的颜色 */ + /*code .keyword { + color: #8959a8; + } + + code .number { + color: #f5871f; + } + + code .comment { + color: #998 + }*/ + + aside { + display: block; + float: right; + width: 390px; + } + blockquote { + border-left:.3em solid #048EFA; + padding: 1em; + margin-left:0; + margin-bottom: 1em; + background: rgba(252, 145, 83, 0.05); + border-radius: 4px; + } + blockquote cite { + font-size:14px; + line-height:20px; + color:#bfbfbf; + } + blockquote cite:before { + content: '\2014 \00A0'; + } + + blockquote p { + margin: 0; + color: #666; + } + hr { + text-align: left; + color: #999; + height: 2px; + padding: 0; + margin: 16px 0; + background-color: #e7e7e7; + border: 0 none; + } + + dl { + padding: 0; + } + + dl dt { + padding: 10px 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: bold; + } + + dl dd { + padding: 0 16px; + margin-bottom: 16px; + } + + dd { + margin-left: 0; + } + + /* Code below this line is copyright Twitter Inc. */ + + button, + input, + select, + textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + } + button, input { + line-height: normal; + *overflow: visible; + } + button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; + } + button, + input[type="button"], + input[type="reset"], + input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + } + input[type=checkbox], input[type=radio] { + cursor: pointer; + } + /* override default chrome & firefox settings */ + input:not([type="image"]), textarea { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + } + + input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + } + input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + label, + input, + select, + textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: normal; + margin-bottom: 18px; + } + input[type=checkbox], input[type=radio] { + cursor: pointer; + margin-bottom: 0; + } + input[type=text], + input[type=password], + textarea, + select { + display: inline-block; + width: 210px; + padding: 4px; + font-size: 13px; + font-weight: normal; + line-height: 18px; + height: 18px; + color: #808080; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + select, input[type=file] { + height: 27px; + line-height: 27px; + } + textarea { + height: auto; + } + /* grey out placeholders */ + :-moz-placeholder { + color: #bfbfbf; + } + ::-webkit-input-placeholder { + color: #bfbfbf; + } + input[type=text], + input[type=password], + select, + textarea { + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + } + input[type=text]:focus, input[type=password]:focus, textarea:focus { + outline: none; + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + } + /* buttons */ + button { + display: inline-block; + padding: 4px 14px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #0064cd; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); + background-image: -moz-linear-gradient(top, #049cdb, #0064cd); + background-image: -ms-linear-gradient(top, #049cdb, #0064cd); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); + background-image: -webkit-linear-gradient(top, #049cdb, #0064cd); + background-image: -o-linear-gradient(top, #049cdb, #0064cd); + background-image: linear-gradient(top, #049cdb, #0064cd); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border: 1px solid #004b9a; + border-bottom-color: #003f81; + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + transition: 0.1s linear all; + border-color: #0064cd #0064cd #003f81; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + } + button:hover { + color: #fff; + background-position: 0 -15px; + text-decoration: none; + } + button:active { + -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + } + button::-moz-focus-inner { + padding: 0; + border: 0; + } + table { + border-collapse: collapse; + width: 100%; + background-color: #fafafa; + font-size: 14px; + margin-bottom: 45px; + line-height: 1.5em; + } + + table td, table th { + border-bottom: .5px solid #ebebeb; + padding: 15px; + max-width: 250px; + font-size: 13px; + code { + font-size: 12px; + } + } + + table th { + text-align: left; + white-space: nowrap; + color: #666; + font-weight: 400; + border: none; + background: #f0f0f0; + } + + // table td:first-child, table th:first-child { + // border-left: solid 1px #ebebeb; + // } + + // table td:last-child, table th:last-child { + // border-right: solid 1px #ebebeb; + // } + + table tr:last-child td { + border: none; + } + + table th:first-child { + -moz-border-radius: 6px 0 0 0; + -webkit-border-radius: 6px 0 0 0; + border-radius: 6px 0 0 0; + border: none; + } + table th:last-child { + padding-right: 1px; + -moz-border-radius: 0 6px 0 0; + -webkit-border-radius: 0 6px 0 0; + border-radius: 0 6px 0 0; + border: none; + } + table th:only-child{ + -moz-border-radius: 6px 6px 0 0; + -webkit-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; + } + table tr:last-child td:first-child { + -moz-border-radius: 0 0 0 6px; + -webkit-border-radius: 0 0 0 6px; + border-radius: 0 0 0 6px; + } + table tr:last-child td:last-child { + -moz-border-radius: 0 0 6px 0; + -webkit-border-radius: 0 0 6px 0; + border-radius: 0 0 6px 0; + } + img { + vertical-align: middle; + max-width: 100%; + } +} \ No newline at end of file diff --git a/site/theme/default/assets/css/mixin.styl b/site/theme/default/assets/css/mixin.styl new file mode 100644 index 00000000..ffa05d22 --- /dev/null +++ b/site/theme/default/assets/css/mixin.styl @@ -0,0 +1,9 @@ +block() + float left + width 100% + +clearfix() + &:after + content "" + clear both + display table \ No newline at end of file diff --git a/site/theme/default/assets/css/toc.styl b/site/theme/default/assets/css/toc.styl new file mode 100644 index 00000000..e39ad93c --- /dev/null +++ b/site/theme/default/assets/css/toc.styl @@ -0,0 +1,46 @@ + .default-doc-toc + position absolute + top 0 + right 0 + width 150px + border-left solid 1px #f0f1f2 + &.is-stricky + position fixed + top 32px + right 0 + .mfe-blog-toc-item + position relative + block() + padding 5px 15px + box-sizing border-box + text-decoration none + color #333 + white-space nowrap + overflow hidden + text-overflow ellipsis + &.mfe-blog-toc-item-h1 + font-size 18px + &.mfe-blog-toc-item-h2 + font-size 16px + &.mfe-blog-toc-item-h3 + // color #333 + font-size 14px + &.mfe-blog-toc-item-h4 + // color #999 + font-size 12px + &.mfe-blog-toc-item-h5 + // color #ccc + font-size 12px + font-weight 300 + &.active::before + content "" + position absolute + left 0 + top 0 + width 2px + height 100% + background-color #fc9153 + +@media (max-width: 750px) + .mfe-blog-theme-default-doc .default-doc-toc + display none \ No newline at end of file diff --git a/site/theme/default/assets/js/home.config.js b/site/theme/default/assets/js/home.config.js new file mode 100644 index 00000000..18c07fa8 --- /dev/null +++ b/site/theme/default/assets/js/home.config.js @@ -0,0 +1,86 @@ +import MfeTable from '../../components/Table' + +const qrcodeTableView = { + name: 'qrcodeTable', + components: { + MfeTable + }, + data () { + return { + qrcodeTableShow: false + } + }, + template: '' +} + +export default [ + { + title: 'Mand Mobile', + describe: '一个基于Vue的移动端UI组件库,丰富、灵活、实用,快速搭建优质的金融类产品,让复杂的金融场景变简单。', + buttons: [{ + type: 'link', + text: '开始使用', + src: '/docs', + theme: 'start' + }, { + type: 'handler', + text: '扫码体验', + click (ref) { + this.$refs[ref][0].qrcodeTableShow = true + }, + theme: 'demo', + slots: qrcodeTableView + }, { + htmls: '' + }], + animations: { + bg: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-0.svg', + content: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-0.png' + } + }, + { + title: '用户体验', + describe: '基于「合理、好用」设计价值观,从交互操作、视觉抽象、图形可视等角度共同解决问题。 ', + animations: { + bg: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-1.svg', + content: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-1.png' + }, + decorate: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-cirlce.svg' + }, + { + title: '敏捷支持', + describe: '汲取最前沿技术,组件化轻量化实现,兼顾稳定和品质,努力实现金融场景的全覆盖。', + animations: [ + { + icon: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-2-0.svg', + title: '丰富的组件', + describe: '30+的基础组件,覆盖金融场景', + }, + { + icon: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-2-1.svg', + title: '极高的易用性', + describe: '组件均有详细说明文档、案例演示', + }, + { + icon: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-2-2.svg', + title: '轻量的Bundle', + describe: '支持babel-plugin-import自动化按需加载代码,减小bundle体积', + } + ] + }, + { + title: '共享资源', + describe: '提供相关资源的下载,输出规范,助力快速搭建优质产品页面原型或高保真视觉稿。', + buttons: [{ + type: 'link', + text: '设计资源', + src: '/design/resource', + theme: 'demo' + }], + animations: { + bg: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-3.svg', + content: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-3.png' + }, + decorate: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-rectangle.svg' + }, +] \ No newline at end of file diff --git a/site/theme/default/assets/js/responsive.js b/site/theme/default/assets/js/responsive.js new file mode 100644 index 00000000..c59e1da4 --- /dev/null +++ b/site/theme/default/assets/js/responsive.js @@ -0,0 +1,26 @@ +(function (window, document) { + + function resize(){ + var ww = window.innerWidth; + if (ww > window.screen.width) { + window.requestAnimationFrame(resize); + } + else{ + if (ww > 720) { + ww = 720 + } + document.documentElement.style.fontSize = ww * 0.13333333333333333 + 'px'; + document.body.style.opacity = 1; + } + } + + if (document.readyState !== 'loading') { + resize(); + } + else { + document.addEventListener('DOMContentLoaded', resize); + } + + window.addEventListener('resize', resize); + + })(window, document) diff --git a/site/theme/default/assets/js/util.js b/site/theme/default/assets/js/util.js new file mode 100644 index 00000000..29b407b5 --- /dev/null +++ b/site/theme/default/assets/js/util.js @@ -0,0 +1,34 @@ +export function findMenu(list, nav) { + let sublist + list.forEach(item => { + if (item.name === nav) { + return (sublist = item.menu) + } + }) + return sublist +} + + +export function setScale (scale) { + const viewPort = document.querySelector('meta[name=viewport]') + + if (!viewPort) { + return + } + + const viewPortContent = viewPort.getAttribute('content') + const viewPortContentParts = viewPortContent.split(',') + + let newViewPortContent = '' + + for (let i = 0, len = viewPortContentParts.length; i < len; i++) { + const attr = viewPortContentParts[i] + if ((attr.indexOf('initial-scale') >= 0) || (attr.indexOf('maximum-scale') >= 0) || (attr.indexOf('minimum-scale') >= 0)) { + continue + } else { + newViewPortContent += `${attr},` + } + } + newViewPortContent += `initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}` + viewPort.setAttribute('content', newViewPortContent) +} \ No newline at end of file diff --git a/site/theme/default/components/Doc.vue b/site/theme/default/components/Doc.vue new file mode 100644 index 00000000..faebb9e4 --- /dev/null +++ b/site/theme/default/components/Doc.vue @@ -0,0 +1,501 @@ + + + + + diff --git a/site/theme/default/components/Footer.vue b/site/theme/default/components/Footer.vue new file mode 100644 index 00000000..ed6aced6 --- /dev/null +++ b/site/theme/default/components/Footer.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/site/theme/default/components/Header.vue b/site/theme/default/components/Header.vue new file mode 100644 index 00000000..e3cee25b --- /dev/null +++ b/site/theme/default/components/Header.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/site/theme/default/components/Menu.vue b/site/theme/default/components/Menu.vue new file mode 100644 index 00000000..804405d6 --- /dev/null +++ b/site/theme/default/components/Menu.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/site/theme/default/components/Table.vue b/site/theme/default/components/Table.vue new file mode 100644 index 00000000..f3403f93 --- /dev/null +++ b/site/theme/default/components/Table.vue @@ -0,0 +1,159 @@ + + + + + + + + diff --git a/site/theme/default/main.js b/site/theme/default/main.js new file mode 100644 index 00000000..46de4a33 --- /dev/null +++ b/site/theme/default/main.js @@ -0,0 +1,19 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import App from './App' +import router from './router' +import { setScale } from './assets/js/util' + +Vue.config.productionTip = false + +if ($(window).width() > 750) { + setScale(0.5) +} + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), + router, +}) diff --git a/site/theme/default/router/index.js b/site/theme/default/router/index.js new file mode 100644 index 00000000..30e0f4cc --- /dev/null +++ b/site/theme/default/router/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Home from '../Home' +import Error from '../Error' +import Routes from '../../../public/route' + +Vue.use(Router) + +Routes.map((item, index) => { + item.meta = item.meta || {} + item.meta.index = index + return item +}) + +const routes = [ + ...Routes, + {path: '/home', component: Home, meta: {noMenu: true}}, + {path: '/', redirect: '/home'}, + {path: '*', component: Error, meta: {noMenu: true}}, +] + +const router = new Router({ + mode: 'history', + base: window.mbConfig.routePrefix, + routes +}) + +router.beforeEach((to, from, next) => { + document.title = (to.meta.text + ? `${to.meta.text}-${window.mbConfig.title}` + : window.mbConfig.title).replace(/<[^>]+>/g, '') + next() +}) + +window.$routes = Routes + +export default router diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/static/animate.css b/static/animate.css new file mode 100644 index 00000000..3cff2ee3 --- /dev/null +++ b/static/animate.css @@ -0,0 +1,16 @@ +@charset "UTF-8";body{-webkit-backface-visibility:hidden;} +.animated{-webkit-animation-duration:1s;-moz-animation-duration:1s;-o-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;} +.animated.hinge{-webkit-animation-duration:2s;-moz-animation-duration:2s;-o-animation-duration:2s;animation-duration:2s;} +@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translateX(-2000px);} +100%{opacity:1;-webkit-transform:translateX(0);} +} +@-moz-keyframes fadeInLeftBig{0%{opacity:0;-moz-transform:translateX(-2000px);} +100%{opacity:1;-moz-transform:translateX(0);} +} +@-o-keyframes fadeInLeftBig{0%{opacity:0;-o-transform:translateX(-2000px);} +100%{opacity:1;-o-transform:translateX(0);} +} +@keyframes fadeInLeftBig{0%{opacity:0;transform:translateX(-2000px);} +100%{opacity:1;transform:translateX(0);} +} +.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;-moz-animation-name:fadeInLeftBig;-o-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig;} diff --git a/static/jquery.lettering.js b/static/jquery.lettering.js new file mode 100644 index 00000000..9124e9bd --- /dev/null +++ b/static/jquery.lettering.js @@ -0,0 +1 @@ +(function($){function injector(t,splitter,klass,after){var a=t.text().split(splitter),inject='';if(a.length){$(a).each(function(i,item){inject+=''+item+''+after});t.empty().append(inject)}}var methods={init:function(){return this.each(function(){injector($(this),'','char','')})},words:function(){return this.each(function(){injector($(this),' ','word',' ')})},lines:function(){return this.each(function(){var r="eefec303079ad17405c889e092e105b0";injector($(this).children("br").replaceWith(r).end(),r,'line','')})}};$.fn.lettering=function(method){if(method&&methods[method]){return methods[method].apply(this,[].slice.call(arguments,1))}else if(method==='letters'||!method){return methods.init.apply(this,[].slice.call(arguments,0))}$.error('Method '+method+' does not exist on jQuery.lettering');return this}})(window.jQuery||window.Zepto); \ No newline at end of file diff --git a/static/jquery.textillate.js b/static/jquery.textillate.js new file mode 100644 index 00000000..45b528b3 --- /dev/null +++ b/static/jquery.textillate.js @@ -0,0 +1 @@ +(function ($) { "use strict"; function isInEffect (effect) { return /In/.test(effect) || $.inArray(effect, $.fn.textillate.defaults.inEffects) >= 0; }; function isOutEffect (effect) { return /Out/.test(effect) || $.inArray(effect, $.fn.textillate.defaults.outEffects) >= 0; }; function stringToBoolean(str) { if (str !== "true" && str !== "false"){ return str; }; return (str === "true"); } function getData (node) { var attrs = node.attributes || [] , data = {}; if (!attrs.length){ return data; } $.each(attrs, function (i, attr) { var nodeName = attr.nodeName.replace(/delayscale/, 'delayScale'); if (/^data-in-*/.test(nodeName)) { data.in = data.in || {}; data.in[nodeName.replace(/data-in-/, '')] = stringToBoolean(attr.nodeValue); } else if (/^data-out-*/.test(nodeName)) { data.out = data.out || {}; data.out[nodeName.replace(/data-out-/, '')] =stringToBoolean(attr.nodeValue); } else if (/^data-*/.test(nodeName)) { data[nodeName.replace(/data-/, '')] = stringToBoolean(attr.nodeValue); } }); return data; } function shuffle (o) { for (var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); return o; } function animate ($t, effect, cb) { $t.addClass('animated ' + effect) .css('visibility', 'visible') .show(); $t.one('animationend webkitAnimationEnd oAnimationEnd', function () { $t.removeClass('animated ' + effect); cb && cb(); }); } function animateTokens ($tokens, options, cb) { var that = this , count = $tokens.length; if (!count) { cb && cb(); return; } if (options.shuffle){ $tokens = shuffle($tokens); } if (options.reverse){ $tokens = $tokens.toArray().reverse(); } $.each($tokens, function (i, t) { var $token = $(t); function complete () { if (isInEffect(options.effect)) { $token.css('visibility', 'visible'); } else if (isOutEffect(options.effect)) { $token.css('visibility', 'hidden'); } count -= 1; if (!count && cb){ cb(); } } var delay = options.sync ? options.delay : options.delay * i * options.delayScale; $token.text() ? setTimeout(function () { animate($token, options.effect, complete) }, delay) : complete(); }); }; var Textillate = function (element, options) { var base = this, $element = $(element); base.init = function () { base.$texts = $element.find(options.selector); if (!base.$texts.length) { base.$texts = $('
  • ' + $element.html() + '
'); $element.html(base.$texts); } base.$texts.hide(); base.$current = $('') .html(base.$texts.find(':first-child').html()) .prependTo($element); if (isInEffect(options.in.effect)) { base.$current.css('visibility', 'hidden'); } else if (isOutEffect(options.out.effect)) { base.$current.css('visibility', 'visible'); } base.setOptions(options); base.timeoutRun = null; setTimeout(function () { base.options.autoStart && base.start(); }, base.options.initialDelay) }; base.setOptions = function (options) { base.options = options; }; base.triggerEvent = function (name) { var e = $.Event(name + '.tlt'); $element.trigger(e, base); return e; }; base.in = function (index, cb) { index = index || 0; var $elem = base.$texts.find(':nth-child(' + ((index||0) + 1) + ')') , options = $.extend(true, {}, base.options, $elem.length ? getData($elem[0]) : {}) , $tokens; $elem.addClass('current'); base.triggerEvent('inAnimationBegin'); base.$current .html($elem.html()) .lettering('words'); if (base.options.type == "char") { base.$current.find('[class^="word"]') .css({ 'display': 'inline-block', '-webkit-transform': 'translate3d(0,0,0)', '-moz-transform': 'translate3d(0,0,0)', '-o-transform': 'translate3d(0,0,0)', 'transform': 'translate3d(0,0,0)' }) .each(function () { $(this).lettering() }); } $tokens = base.$current .find('[class^="' + base.options.type + '"]') .css('display', 'inline-block'); if (isInEffect(options.in.effect)) { $tokens.css('visibility', 'hidden'); } else if (isOutEffect(options.in.effect)) { $tokens.css('visibility', 'visible'); } base.currentIndex = index; animateTokens($tokens, options.in, function () { base.triggerEvent('inAnimationEnd'); if (options.in.callback) { options.in.callback(); } if (cb) { cb(base); } }); }; base.out = function (cb) { var $elem = base.$texts.find(':nth-child(' + ((base.currentIndex||0) + 1) + ')') , $tokens = base.$current.find('[class^="' + base.options.type + '"]') , options = $.extend(true, {}, base.options, $elem.length ? getData($elem[0]) : {}); base.triggerEvent('outAnimationBegin'); animateTokens($tokens, options.out, function () { $elem.removeClass('current'); base.triggerEvent('outAnimationEnd'); if (options.out.callback) { options.out.callback(); } if (cb) { cb(base); } }); }; base.start = function (index) { setTimeout(function () { base.triggerEvent('start'); (function run (index) { base.in(index, function () { var length = base.$texts.children().length; index += 1; if (!base.options.loop && index >= length) { if (base.options.callback) base.options.callback(); base.triggerEvent('end'); } else { index = index % length; base.timeoutRun = setTimeout(function () { base.out(function () { run(index) }); }, base.options.minDisplayTime); } }); }(index || 0)); }, base.options.initialDelay); }; base.stop = function () { if (base.timeoutRun) { clearInterval(base.timeoutRun); base.timeoutRun = null; } }; base.init(); }; $.fn.textillate = function (settings, args) { return this.each(function () { var $this = $(this) , data = $this.data('textillate') , options = $.extend(true, {}, $.fn.textillate.defaults, getData(this), typeof settings == 'object' && settings); if (!data) { $this.data('textillate', (data = new Textillate(this, options))); } else if (typeof settings == 'string') { data[settings].apply(data, [].concat(args)); } else { data.setOptions.call(data, options); } }) }; $.fn.textillate.defaults = { selector: '.texts', loop: false, minDisplayTime: 2000, initialDelay: 0, in: { effect: 'fadeInLeftBig', delayScale: 1.5, delay: 50, sync: false, reverse: false, shuffle: false, callback: function () {} }, out: { effect: 'hinge', delayScale: 1.5, delay: 50, sync: false, reverse: false, shuffle: false, callback: function () {} }, autoStart: true, inEffects: [], outEffects: [ 'hinge' ], callback: function () {}, type: 'char' }; }(window.jQuery||window.Zepto)); \ No newline at end of file diff --git a/static/pace.css b/static/pace.css new file mode 100644 index 00000000..b2dfca60 --- /dev/null +++ b/static/pace.css @@ -0,0 +1 @@ +.pace{-webkit-pointer-events:none;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pace-inactive{display:none}.pace .pace-progress{position:fixed;top:0;right:100%;z-index:2000;width:100%;height:2px;background:#fc9153;opacity:1}.pace .pace-progress-inner{position:absolute;right:0;display:block;width:75pt;height:100%;opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px)}.pace .pace-activity{position:fixed;top:10px;right:10px;z-index:2000;display:block;width:10px;height:10px;border:2px solid transparent;border-radius:10px;border-top-color:#fc9153;border-left-color:#fc9153;-webkit-animation:pace-spinner .4s linear infinite;animation:pace-spinner .4s linear infinite}@-webkit-keyframes pace-spinner{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes pace-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}} \ No newline at end of file diff --git a/static/pace.js b/static/pace.js new file mode 100644 index 00000000..c47d6e5a --- /dev/null +++ b/static/pace.js @@ -0,0 +1,2 @@ +/*! pace 1.0.0 */ +(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
\n
\n
\n
',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e,f;f=[];for(d in b.prototype)try{e=b.prototype[d],f.push(null==a[d]&&"function"!=typeof e?a[d]=e:void 0)}catch(g){c=g}return f},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); \ No newline at end of file diff --git a/test/unit/.eslintrc b/test/unit/.eslintrc new file mode 100644 index 00000000..959a4f4b --- /dev/null +++ b/test/unit/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { + "mocha": true + }, + "globals": { + "expect": true, + "sinon": true + } +} diff --git a/test/unit/index.js b/test/unit/index.js new file mode 100644 index 00000000..59c1a417 --- /dev/null +++ b/test/unit/index.js @@ -0,0 +1,8 @@ +import Vue from 'vue' + +/* eslint-disable no-undef */ +Vue.config.productionTip = false + +// require all test files (files that ends with .spec.js) +const testsContext = require.context('../../components', true, /\.spec$/) +testsContext.keys().forEach(testsContext) diff --git a/test/unit/index.rollup.js b/test/unit/index.rollup.js new file mode 100644 index 00000000..3cd88815 --- /dev/null +++ b/test/unit/index.rollup.js @@ -0,0 +1 @@ +import '../../components/*/test/*.spec.js' \ No newline at end of file diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js new file mode 100644 index 00000000..6e598f0f --- /dev/null +++ b/test/unit/karma.conf.js @@ -0,0 +1,47 @@ +// This is a karma config file. For more details see +// http://karma-runner.github.io/0.13/config/configuration-file.html +// we are also using it with karma-webpack +// https://github.com/webpack/karma-webpack +const webpackConfig = require('../../build/webpack/webpack.test.conf') +const merge = require('webpack-merge') +const webpack = require('webpack') +const scope = process.argv[4] || '' +module.exports = function(config) { + config.set({ + // to run in additional browsers: + // 1. install corresponding karma launcher + // http://karma-runner.github.io/0.13/config/browsers.html + // 2. add it to the `browsers` array below. + browsers: ['PhantomJS'], + frameworks: ['mocha', 'sinon-chai'], + reporters: ['spec', 'coverage'], + files: ['./index.js'], + preprocessors: { + './index.js': ['webpack', 'sourcemap'], + [`components/${scope}/!(demo)/*.vue`]: ['coverage'], + 'components/!(_util)/*.js': ['coverage'], + }, + concurrency: 1, + webpack: merge( + { + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"testing"', + SCOPE: `"${scope}"`, + }, + }), + ], + }, + webpackConfig + ), + webpackMiddleware: { + // noInfo: true + }, + coverageReporter: { + dir: '../../output/coverage', + reporters: [{type: 'lcov', subdir: '.'}, {type: 'text'}], + includeAllSources: false, + }, + }) +} diff --git a/test/unit/rollup.karma.conf.js b/test/unit/rollup.karma.conf.js new file mode 100644 index 00000000..29ddc0d4 --- /dev/null +++ b/test/unit/rollup.karma.conf.js @@ -0,0 +1,52 @@ +const { rollupPlugin } = require('../../build/rollup/rollup-plugin-config') +// This is a karma config file. For more details see +// http://karma-runner.github.io/0.13/config/configuration-file.html +// we are also using it with karma-webpack +// https://github.com/webpack/karma-webpack +const scope = process.argv[5] || '' +let file = './index.rollup.js' +if (scope) { + file = `../../components/${scope}/test/*.spec.js` +} + +module.exports = function(config) { + config.set({ + // to run in additional browsers: + // 1. install corresponding karma launcher + // http://karma-runner.github.io/0.13/config/browsers.html + // 2. add it to the `browsers` array below. + singleRun: true, + browsers: ['PhantomJS'], + // basePath: '../../', + frameworks: ['mocha', 'sinon-chai'], + reporters: ['spec', 'coverage'], + files: [{ + pattern: file, + watched: false, + }], + preprocessors: { + // './index.js': ['webpack', 'sourcemap'], + // 'components/*/test/*.spec.js': ['rollup'], + [file]: ['rollup'] + // [`components/${scope}/!(demo)/*.vue`]: ['coverage'], + // 'components/!(_util)/*.js': ['coverage'], + }, + client: { + clearContext: true, + }, + rollupPreprocessor: { + output: { + // file: 'bundle.js', + format: 'iife', + name: 'mand', + // sourcemap: 'inline', + }, + plugins: rollupPlugin, + }, + coverageReporter: { + dir: '../../docs/coverage', + reporters: [{type: 'lcov', subdir: '.'}, {type: 'text'}], + includeAllSources: false, + }, + }) +} diff --git a/types/component.d.ts b/types/component.d.ts new file mode 100644 index 00000000..5545653b --- /dev/null +++ b/types/component.d.ts @@ -0,0 +1,5 @@ +import Vue from 'vue' + +export class MandComponent extends Vue { + static name: string +} \ No newline at end of file diff --git a/types/dialog.d.ts b/types/dialog.d.ts new file mode 100644 index 00000000..6a1a2052 --- /dev/null +++ b/types/dialog.d.ts @@ -0,0 +1,27 @@ +import Vue from 'vue' + +export type DialogOptions = { + title?: string + content?: string + confirmText?: string + onConfirm?: () => void +} + +export type DialogAlertOptions = { + icon?: string +} & DialogOptions + + +export type DialogConfirmOptions = { + cancelText?: string +} & DialogAlertOptions + +export interface Dialog { + confirm(options: DialogConfirmOptions): Vue + alert(options: DialogAlertOptions): Vue + succeed(options: DialogOptions): Vue + failed(options: DialogOptions): Vue + close(): void +} + +export const Dialog: Dialog \ No newline at end of file diff --git a/types/image-processor.d.ts b/types/image-processor.d.ts new file mode 100644 index 00000000..ed4358a6 --- /dev/null +++ b/types/image-processor.d.ts @@ -0,0 +1,15 @@ +export interface ImageProcessorOptions { + dataUrl: string + width?: number + height?: number + quality: number +} + +export interface ImageProcessorData { + dataUrl: string + blob: Blob +} + +export declare function ImageProcessor( + options: ImageProcessorOptions, + callback?: (data: ImageProcessorData) => any): Promise diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..7d82dc49 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,59 @@ +import Vue from 'vue' +import { MandComponent } from './component' +import { Toast } from './toast' +import imageProcessor from './image-processor' +import { Dialog } from './dialog' + + +export function install(vue: typeof Vue): void + +export class ActionBar extends MandComponent { } +export class ActionSheet extends MandComponent { } +export class Agree extends MandComponent { } +export class Button extends MandComponent { } +export class Captcha extends MandComponent { } +export class Cashier extends MandComponent { } +export class Chart extends MandComponent { } +export class Codebox extends MandComponent { } +export class DatePicker extends MandComponent { } +export class DropMenu extends MandComponent { } +export class Field extends MandComponent { } +export class FieldItem extends MandComponent { } +export class Icon extends MandComponent { } +export class ImageReader extends MandComponent { } +export class ImageViewer extends MandComponent { } +export class InputItem extends MandComponent { } +export class Landscape extends MandComponent { } +export class NoticeBar extends MandComponent { } +export class NumberKeyboard extends MandComponent { } +export class Picker extends MandComponent { } +export class Popup extends MandComponent { } +export class PopupTitleBar extends MandComponent { } +export class Radio extends MandComponent { } +export class ResultPage extends MandComponent { } +export class Selector extends MandComponent { } +export class Stepper extends MandComponent { } +export class Steps extends MandComponent { } +export class Swiper extends MandComponent { } +export class SwiperItem extends MandComponent { } +export class Switch extends MandComponent { } +export class TabBar extends MandComponent { } +export class TabPicker extends MandComponent { } +export class Tabs extends MandComponent { } +export class Tag extends MandComponent { } +export class Tip extends MandComponent { } + + +// declare module 'mand-mobile/lib/image-reader/image-processor' { +// export = imageProcessor +// /** +// * export image processor options +// */ +// export interface ImageProcessorOptions extends imageProcessor.ImageProcessorOptions { } +// export interface ImageProcessorData extends imageProcessor.ImageProcessorData { } +// } + +export { + Toast, + Dialog, +} \ No newline at end of file diff --git a/types/toast.d.ts b/types/toast.d.ts new file mode 100644 index 00000000..eeb5ab72 --- /dev/null +++ b/types/toast.d.ts @@ -0,0 +1,19 @@ +export type ToastOptions = { + content: string + duration: number + parentNode: Element +} + +export type ToastConstructorOptions = { + icon: string +} & ToastOptions + +export interface Toast { + (options?: ToastConstructorOptions): void + succeed(options?: ToastOptions): void + failed(options?: ToastOptions): void + loading(options?: ToastOptions): void + hide(): void +} + +export const Toast: Toast \ No newline at end of file