From 1c17f09da8345a617146110003a72a2496e066c8 Mon Sep 17 00:00:00 2001 From: wangyaju Date: Mon, 12 Jul 2021 11:22:27 +0800 Subject: [PATCH] chore(release): release 11.4.0 (#125) Co-authored-by: huaweidevcloud --- angular.json | 3 +- .../src/sidebar/sidebar.component.html | 8 +- .../src/sidebar/sidebar.component.scss | 8 + .../src/sidebar/sidebar.component.ts | 4 + devui/alert/alert.component.html | 2 +- devui/alert/alert.component.scss | 11 + devui/alert/alert.types.ts | 2 +- devui/alert/demo/basic/basic.component.html | 1 + devui/alert/demo/close/close.component.html | 1 + .../withoutIcon/withoutIcon.component.html | 1 + devui/alert/doc/api-cn.md | 2 +- devui/alert/doc/api-en.md | 2 +- devui/anchor/anchor-link.directive.ts | 3 + devui/anchor/anchor.directive.ts | 2 +- devui/anchor/demo/async/async.component.html | 8 +- devui/anchor/demo/basic/basic.component.html | 8 +- devui/anchor/demo/hash/hash.component.html | 8 +- .../scroll-target.component.html | 8 +- devui/back-top/back-top.component.ts | 14 +- .../scroll-container.component.ts | 7 +- devui/button/button.component.scss | 2 +- devui/button/button.component.ts | 5 +- .../demo/autofocus/autofocus.component.html | 2 +- .../combination/combination.component.css | 2 +- .../button/demo/common/common.component.html | 2 +- .../button/demo/groups/groups.component.html | 6 +- devui/button/demo/icon/icon.component.scss | 2 +- .../demo/left-right/left-right.component.html | 2 +- .../demo/primary/primary.component.html | 2 +- devui/button/demo/size/size.component.html | 6 +- devui/card/demo/basic/basic.component.html | 2 +- devui/card/demo/basic/basic.component.scss | 10 + devui/card/demo/custom/custom.component.scss | 6 + .../demo/with-media/with-media.component.scss | 6 + devui/cascader/cascader-li.component.html | 3 +- devui/cascader/cascader.component.html | 4 + devui/cascader/cascader.component.ts | 4 + .../cascader/demo/basic/basic.component.html | 12 +- .../demo/cascader-demo.component.html | 7 + .../cascader/demo/cascader-demo.component.ts | 7 + devui/cascader/demo/cascader-demo.module.ts | 4 + .../cascader-header-template.component.html | 26 + .../cascader-header-template.component.scss | 3 + .../cascader-header-template.component.ts | 322 +++++++++++++ devui/cascader/doc/api-cn.md | 35 +- devui/cascader/doc/api-en.md | 3 +- .../category-search.component.html | 58 ++- .../category-search.component.scss | 4 + .../category-search.component.ts | 66 ++- .../category-search/category-search.module.ts | 4 +- .../demo/basic/basic.component.ts | 7 +- devui/category-search/doc/api-cn.md | 38 +- devui/category-search/doc/api-en.md | 36 +- devui/common/clipboard.directive.ts | 14 +- devui/common/demo/common-demo.component.html | 14 +- devui/common/demo/common-demo.component.ts | 2 +- .../helper-download.component.html | 2 +- .../iframe-propagate.component.ts | 10 +- .../demo/lazy-load/lazy-load.component.html | 11 +- .../demo/lazy-load/lazy-load.component.ts | 24 + devui/common/doc/api-cn.md | 3 +- devui/common/doc/api-en.md | 1 + devui/common/helper-utils.ts | 8 +- .../iframe-event-propagate.directive.ts | 9 +- .../data-table/data-table-body.component.html | 2 +- devui/data-table/data-table-head.component.ts | 88 ++-- .../data-table/data-table-row.component.html | 2 +- .../data-table/data-table-row.component.scss | 1 + devui/data-table/data-table.component.ts | 25 +- .../data-table-demo-async.component.html | 2 +- .../demo/tree-table/tree-data.component.html | 6 +- .../table/body/td/td.component.scss | 1 + .../table/head/th/filter/filter.component.ts | 14 +- .../data-table/table/head/th/th.component.ts | 16 +- .../data-table/table/head/thead.component.ts | 21 +- .../datepicker-panel.component.html | 41 ++ .../datepicker-panel.component.scss | 15 + .../datepicker-panel.component.ts | 27 ++ .../datepicker-pro-calendar.component.html | 6 + .../datepicker-pro-calendar.component.scss | 3 + .../datepicker-pro-calendar.component.ts | 211 ++++++++ .../datepicker-pro.component.html | 59 +++ .../datepicker-pro.component.scss | 46 ++ .../datepicker-pro.component.ts | 302 ++++++++++++ devui/datepicker-pro/datepicker-pro.module.ts | 48 ++ .../datepicker-pro/datepicker-pro.service.ts | 250 ++++++++++ .../basic/basic-datepicker-pro.component.html | 13 + .../basic/basic-datepicker-pro.component.ts | 19 + .../demo/datepicker-pro-demo.component.html | 67 +++ .../demo/datepicker-pro-demo.component.ts | 96 ++++ .../demo/datepicker-pro-demo.module.ts | 66 +++ .../datepicker-host-template.component.html | 26 + .../datepicker-host-template.component.ts | 18 + .../month-year-picker.component.html | 7 + .../month-year-picker.component.ts | 16 + .../range-template.component.html | 21 + .../range-template.component.scss | 12 + .../range-template.component.ts | 44 ++ .../range-type-picker.component.html | 19 + .../range-type/range-type-picker.component.ts | 25 + .../select-type/select-type.component.html | 26 + .../select-type/select-type.component.scss | 35 ++ .../demo/select-type/select-type.component.ts | 52 ++ .../show-time/show-time-picker.component.html | 2 + .../show-time/show-time-picker.component.ts | 14 + ...datepicker-pro-static-panel.component.html | 5 + ...datepicker-pro-static-panel.component.scss | 5 + .../datepicker-pro-static-panel.component.ts | 18 + .../datepicker-template.component.html | 35 ++ .../datepicker-template.component.scss | 23 + .../template/datepicker-template.component.ts | 32 ++ devui/datepicker-pro/doc/api-cn.md | 97 ++++ devui/datepicker-pro/doc/api-en.md | 96 ++++ devui/datepicker-pro/index.ts | 1 + .../lib/calendar-panel.component.html | 73 +++ .../lib/calendar-panel.component.scss | 253 ++++++++++ .../lib/calendar-panel.component.ts | 452 ++++++++++++++++++ .../datepicker-pro/lib/datepicker-pro.type.ts | 21 + .../lib/footer-panel.component.html | 8 + .../lib/footer-panel.component.scss | 12 + .../lib/footer-panel.component.ts | 71 +++ .../lib/month-panel.component.html | 51 ++ .../lib/month-panel.component.scss | 143 ++++++ .../lib/month-panel.component.ts | 288 +++++++++++ .../lib/timepicker-panel.component.html | 48 ++ .../lib/timepicker-panel.component.scss | 95 ++++ .../lib/timepicker-panel.component.ts | 207 ++++++++ .../lib/year-panel.component.html | 25 + .../lib/year-panel.component.scss | 109 +++++ .../lib/year-panel.component.ts | 226 +++++++++ devui/datepicker-pro/package.json | 8 + devui/datepicker-pro/public-api.ts | 12 + .../range-datepicker-pro.component.html | 80 ++++ .../range-datepicker-pro.component.scss | 70 +++ .../range-datepicker-pro.component.ts | 412 ++++++++++++++++ .../datepicker/date-range-picker.directive.ts | 14 +- .../datepicker-cdk-overlay.component.ts | 13 +- devui/datepicker/datepicker.directive.ts | 154 +++--- .../two-datepicker.component.ts | 13 +- .../border-radius.component.html | 2 +- .../border-radius/border-radius.component.ts | 10 +- .../color/demo/color/color.component.html | 2 +- .../color/demo/color/color.component.ts | 10 +- .../font/demo/font/font.component.html | 4 +- .../font/demo/font/font.component.ts | 10 +- .../shadow/demo/shadow/shadow.component.html | 4 +- .../shadow/demo/shadow/shadow.component.ts | 10 +- devui/devui.module.ts | 3 + .../demo/follow/follow.component.html | 2 +- devui/dragdrop/demo/tree/tree.component.html | 2 +- .../directives/draggable.directive.ts | 17 +- .../drop-scroll-enhance.directive.ts | 25 +- .../directives/droppable.directive.ts | 10 +- devui/dragdrop/services/drag-drop.service.ts | 23 +- .../dragdrop/touch-support/dragdrop-touch.ts | 48 +- devui/drawer/demo/drawer-demo.component.html | 7 + devui/drawer/demo/drawer-demo.component.ts | 6 + devui/drawer/demo/drawer-demo.module.ts | 3 +- .../demo/template/template.component.html | 11 + .../demo/template/template.component.ts | 35 ++ .../undestroyable.component.html | 2 +- .../undestroyable/undestroyable.component.ts | 7 +- devui/drawer/doc/api-cn.md | 5 +- devui/drawer/doc/api-en.md | 3 +- devui/drawer/drawer.component.html | 3 + devui/drawer/drawer.component.ts | 55 ++- devui/drawer/drawer.service.ts | 23 +- devui/drawer/drawer.types.ts | 4 +- devui/dropdown/dropdown-menu.directive.ts | 15 +- devui/dropdown/dropdown.directive.ts | 13 +- devui/dropdown/dropdown.service.ts | 11 +- .../form/services/d-validate-sync.service.ts | 3 +- devui/fullscreen/fullscreen.component.ts | 25 +- devui/gantt/demo/basic/basic.component.html | 5 +- devui/gantt/demo/basic/basic.component.ts | 5 +- devui/gantt/demo/table/table.component.html | 6 +- devui/gantt/demo/table/table.component.scss | 5 +- devui/gantt/demo/table/table.component.ts | 13 +- devui/gantt/gantt-bar/gantt-bar.component.ts | 80 +++- .../gantt-scale/gantt-scale.component.html | 4 +- .../gantt-scale/gantt-scale.component.scss | 12 +- .../gantt-scale/gantt-scale.component.ts | 74 ++- .../gantt-tools/gantt-tools.component.html | 22 +- .../gantt-tools/gantt-tools.component.scss | 9 + .../gantt-tools/gantt-tools.component.ts | 25 +- devui/gantt/gantt.model.ts | 1 + devui/gantt/gantt.service.ts | 6 +- devui/gantt/gantt.spec.ts | 69 +-- devui/gantt/public-api.ts | 13 +- devui/gantt/resize-handle.directive.ts | 16 +- devui/i18n/en-us.ts | 4 +- devui/i18n/i18n.model.ts | 3 + devui/i18n/zh-cn.ts | 4 +- .../demo/z-index/z-index.component.html | 8 +- .../image-preview/image-preview.component.ts | 15 +- devui/image-preview/transformable-element.ts | 16 +- .../demo/input-number-demo.component.html | 2 +- devui/input-number/input-number.component.ts | 26 +- .../loading/demo/custom/custom.component.html | 2 +- .../demo/full-screen/full-screen.component.ts | 7 +- devui/loading/loading.service.ts | 15 +- devui/mention/utils.ts | 4 +- .../basic-update/basic-update.component.html | 2 +- devui/modal/demo/basic/basic.component.html | 4 +- .../demo/fixed/fixed-wrapper.component.ts | 4 +- devui/modal/demo/hide/hide.component.html | 2 +- .../demo/template/template.component.html | 4 +- devui/modal/demo/tips/tips.component.html | 8 +- devui/modal/dialog.service.ts | 23 +- devui/modal/modal.component.ts | 24 +- devui/modal/modal.service.ts | 26 +- devui/modal/movable.directive.ts | 4 +- ...ulti-auto-complete-demo-array.component.ts | 12 +- devui/nav-sprite/nav-sprite.component.ts | 16 +- devui/package.json | 2 +- .../demo/additional/additional.component.html | 14 +- devui/pagination/doc/api-en.md | 2 +- devui/pagination/pagination.component.ts | 2 +- devui/panel/demo/basic/basic.component.html | 8 + devui/panel/doc/api-cn.md | 25 +- devui/panel/doc/api-en.md | 21 +- devui/panel/panel.component.html | 2 +- devui/panel/panel.component.scss | 18 +- devui/panel/panel.component.ts | 1 + devui/popover/demo/basic/basic.component.css | 4 - devui/popover/demo/basic/basic.component.html | 34 +- devui/popover/demo/basic/basic.component.scss | 16 + devui/popover/demo/basic/basic.component.ts | 2 +- .../demo/customize/customize.component.html | 1 + .../hover-delay-time.component.html | 9 +- devui/popover/demo/popover-demo.component.ts | 2 +- .../scroll-element.component.ts | 7 +- devui/popover/popover.component.ts | 52 +- devui/popover/popover.directive.ts | 12 +- devui/portal/portal.component.ts | 25 +- devui/position/positioning.service.ts | 9 +- .../demo/basic/basic.component.scss | 2 +- .../demo/config/config.component.scss | 2 +- .../demo/config/config.component.ts | 8 +- .../quadrant-axis/quadrant-axis.component.ts | 2 + .../quadrant-diagram.component.scss | 1 + .../quadrant-diagram.component.ts | 2 +- .../quadrant-diagram.service.ts | 22 +- devui/radio/doc/api-cn.md | 5 +- devui/radio/doc/api-en.md | 5 +- devui/read-tip/read-tip.directive.ts | 16 +- devui/select/select.component.ts | 11 +- devui/shared/devui-api/devui-api.component.ts | 19 +- .../devui-codebox.component.scss | 4 +- .../devui-codebox/devui-codebox.component.ts | 13 +- devui/splitter/splitter-bar.component.html | 7 +- devui/splitter/splitter-bar.component.ts | 16 +- .../demo/basic/basic.component.html | 1 + .../steps-guide/demo/basic/basic.component.ts | 12 +- .../demo/custom/custom.component.ts | 10 +- devui/steps-guide/doc/api-cn.md | 32 +- devui/steps-guide/doc/api-en.md | 34 +- devui/steps-guide/steps-guide.component.scss | 4 - devui/steps-guide/steps-guide.component.ts | 17 +- devui/steps-guide/steps-guide.directive.ts | 49 +- devui/sticky/demo/basic/basic.component.html | 4 +- devui/sticky/demo/basic/basic.component.ts | 7 +- .../scroll-target.component.html | 4 +- .../scroll-target/scroll-target.component.ts | 7 +- devui/sticky/sticky.component.ts | 34 +- devui/sticky/sticky.module.ts | 3 +- devui/style/font/Oswald-VariableFont_wght.ttf | Bin 0 -> 153152 bytes devui/tabs/demo/tabs-demo.component.html | 4 +- devui/tabs/demo/tabs-demo.component.ts | 3 +- .../textarea/demo/count/count.component.html | 4 + .../textarea/demo/count/count.component.scss | 3 + devui/textarea/demo/count/count.component.ts | 25 + devui/textarea/demo/text-demo.component.html | 7 + devui/textarea/demo/text-demo.component.ts | 6 + devui/textarea/demo/text-demo.module.ts | 6 +- .../alternative-mode.component.html | 4 +- .../alternative-mode.component.ts | 16 +- .../demo/custom-dot/custom-dot.component.html | 2 +- .../time-axis-direction.component.html | 4 +- .../time-axis-direction.component.ts | 16 +- .../time-axis-html-content.component.ts | 8 +- .../seperate-way/seperate-way.component.html | 10 +- .../time-axis-template-content.component.html | 10 +- .../time-axis-template-content.component.ts | 22 +- devui/time-axis/demo/time-axis-demo.module.ts | 2 +- devui/time-axis/doc/api-cn.md | 18 +- devui/time-axis/doc/api-en.md | 19 +- .../time-axis-item.component.html | 27 +- .../time-axis-item.component.scss | 50 +- .../time-axis-item.component.ts | 67 ++- devui/time-axis/time-axis.component.html | 7 +- devui/time-axis/time-axis.component.ts | 31 ++ devui/time-axis/time-axis.spec.ts | 4 +- devui/time-axis/time-axis.type.ts | 6 +- devui/time-picker/time-picker.component.ts | 17 +- .../demo/service/toast-service.component.html | 4 +- devui/toggle/toggle.component.scss | 1 + devui/tooltip/demo/basic/basic.component.html | 10 +- devui/tooltip/demo/delay/delay.component.html | 4 +- devui/tooltip/tooltip.directive.ts | 3 +- .../transfer-demo-search.component.html | 1 + devui/transfer/doc/api-cn.md | 5 +- devui/transfer/doc/api-en.md | 5 +- devui/transfer/transfer.component.html | 10 + devui/transfer/transfer.component.scss | 18 +- devui/transfer/transfer.component.ts | 78 +-- devui/transfer/transfer.module.ts | 7 +- devui/tree-select/tree-select.component.scss | 13 +- devui/tree-select/tree-select.component.ts | 6 +- .../demo/checkable/checkable.component.html | 2 + .../demo/draggable/draggable.component.html | 4 +- .../demo/draggable/draggable.component.ts | 2 +- .../operate-btn/operate-btn.component.html | 2 - devui/tree/doc/api-cn.md | 4 +- devui/tree/doc/api-en.md | 4 +- devui/tree/operable-tree.component.ts | 3 + devui/tree/tree-factory.class.ts | 6 +- devui/upload/demo/basic/basic.component.ts | 7 +- devui/upload/select-files.utils.ts | 20 +- devui/upload/upload.class.ts | 10 +- devui/utils/animations/pop-in-out.ts | 4 +- devui/utils/animations/scrollAnimation.ts | 3 + devui/utils/highlight/highlight.component.ts | 19 +- devui/utils/lazy-load/lazy-load.directive.ts | 9 +- devui/utils/popper/popper.component.ts | 12 +- devui/utils/testing/event-helper.ts | 3 + devui/version.ts | 2 +- devui/window-ref/document-ref.service.ts | 13 +- devui/window-ref/window-ref.service.ts | 18 +- package.json | 5 +- .../babel-loader-wepack-config.js | 0 .../es6-only-third-party-list.js | 0 scripts/extra-webpack.config.js | 4 +- scripts/move-assets.js | 3 - src/app/component/app-content.component.html | 2 +- src/app/component/app-content.component.ts | 68 ++- src/app/component/component.route.ts | 12 + src/app/component/d-demo-nav.component.ts | 7 +- .../component/example-panel.component.html | 2 +- src/app/component/example-panel.component.ts | 10 +- src/app/component/get-started.component.ts | 8 +- src/app/component/getStarted-en.md | 2 +- src/app/component/global-config.component.ts | 12 +- src/app/component/overview.component.html | 84 +++- src/app/component/overview.component.scss | 58 ++- src/app/component/overview.component.ts | 94 ++-- .../resolve-routes-config.service.ts | 6 +- src/app/component/scope-list.ts | 26 + src/app/component/theme-guide.component.ts | 8 +- .../customize-theme.component.ts | 25 +- src/app/theme-picker/theme-data-more.ts | 97 +--- .../theme-picker/theme-picker.component.html | 2 +- .../theme-picker/theme-picker.component.ts | 37 +- src/assets/i18n/en-us.json | 37 +- src/assets/i18n/zh-cn.json | 48 +- .../overview/data-entry/datepickerpro.png | Bin 0 -> 6355 bytes src/main.ts | 11 +- src/styles.scss | 8 +- 358 files changed, 7533 insertions(+), 1320 deletions(-) create mode 100644 devui/cascader/demo/header-template/cascader-header-template.component.html create mode 100644 devui/cascader/demo/header-template/cascader-header-template.component.scss create mode 100644 devui/cascader/demo/header-template/cascader-header-template.component.ts create mode 100644 devui/datepicker-pro/datepicker-panel.component.html create mode 100644 devui/datepicker-pro/datepicker-panel.component.scss create mode 100644 devui/datepicker-pro/datepicker-panel.component.ts create mode 100644 devui/datepicker-pro/datepicker-pro-calendar.component.html create mode 100644 devui/datepicker-pro/datepicker-pro-calendar.component.scss create mode 100644 devui/datepicker-pro/datepicker-pro-calendar.component.ts create mode 100644 devui/datepicker-pro/datepicker-pro.component.html create mode 100644 devui/datepicker-pro/datepicker-pro.component.scss create mode 100644 devui/datepicker-pro/datepicker-pro.component.ts create mode 100644 devui/datepicker-pro/datepicker-pro.module.ts create mode 100644 devui/datepicker-pro/datepicker-pro.service.ts create mode 100644 devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.html create mode 100644 devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.ts create mode 100644 devui/datepicker-pro/demo/datepicker-pro-demo.component.html create mode 100644 devui/datepicker-pro/demo/datepicker-pro-demo.component.ts create mode 100644 devui/datepicker-pro/demo/datepicker-pro-demo.module.ts create mode 100644 devui/datepicker-pro/demo/host-template/datepicker-host-template.component.html create mode 100644 devui/datepicker-pro/demo/host-template/datepicker-host-template.component.ts create mode 100644 devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.html create mode 100644 devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.ts create mode 100644 devui/datepicker-pro/demo/range-template/range-template.component.html create mode 100644 devui/datepicker-pro/demo/range-template/range-template.component.scss create mode 100644 devui/datepicker-pro/demo/range-template/range-template.component.ts create mode 100644 devui/datepicker-pro/demo/range-type/range-type-picker.component.html create mode 100644 devui/datepicker-pro/demo/range-type/range-type-picker.component.ts create mode 100644 devui/datepicker-pro/demo/select-type/select-type.component.html create mode 100644 devui/datepicker-pro/demo/select-type/select-type.component.scss create mode 100644 devui/datepicker-pro/demo/select-type/select-type.component.ts create mode 100644 devui/datepicker-pro/demo/show-time/show-time-picker.component.html create mode 100644 devui/datepicker-pro/demo/show-time/show-time-picker.component.ts create mode 100644 devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.html create mode 100644 devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.scss create mode 100644 devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.ts create mode 100644 devui/datepicker-pro/demo/template/datepicker-template.component.html create mode 100644 devui/datepicker-pro/demo/template/datepicker-template.component.scss create mode 100644 devui/datepicker-pro/demo/template/datepicker-template.component.ts create mode 100644 devui/datepicker-pro/doc/api-cn.md create mode 100644 devui/datepicker-pro/doc/api-en.md create mode 100644 devui/datepicker-pro/index.ts create mode 100644 devui/datepicker-pro/lib/calendar-panel.component.html create mode 100644 devui/datepicker-pro/lib/calendar-panel.component.scss create mode 100644 devui/datepicker-pro/lib/calendar-panel.component.ts create mode 100644 devui/datepicker-pro/lib/datepicker-pro.type.ts create mode 100644 devui/datepicker-pro/lib/footer-panel.component.html create mode 100644 devui/datepicker-pro/lib/footer-panel.component.scss create mode 100644 devui/datepicker-pro/lib/footer-panel.component.ts create mode 100644 devui/datepicker-pro/lib/month-panel.component.html create mode 100644 devui/datepicker-pro/lib/month-panel.component.scss create mode 100644 devui/datepicker-pro/lib/month-panel.component.ts create mode 100644 devui/datepicker-pro/lib/timepicker-panel.component.html create mode 100644 devui/datepicker-pro/lib/timepicker-panel.component.scss create mode 100644 devui/datepicker-pro/lib/timepicker-panel.component.ts create mode 100644 devui/datepicker-pro/lib/year-panel.component.html create mode 100644 devui/datepicker-pro/lib/year-panel.component.scss create mode 100644 devui/datepicker-pro/lib/year-panel.component.ts create mode 100644 devui/datepicker-pro/package.json create mode 100644 devui/datepicker-pro/public-api.ts create mode 100644 devui/datepicker-pro/range-datepicker-pro.component.html create mode 100644 devui/datepicker-pro/range-datepicker-pro.component.scss create mode 100644 devui/datepicker-pro/range-datepicker-pro.component.ts create mode 100644 devui/drawer/demo/template/template.component.html create mode 100644 devui/drawer/demo/template/template.component.ts delete mode 100755 devui/popover/demo/basic/basic.component.css create mode 100644 devui/popover/demo/basic/basic.component.scss create mode 100644 devui/style/font/Oswald-VariableFont_wght.ttf create mode 100644 devui/textarea/demo/count/count.component.html create mode 100644 devui/textarea/demo/count/count.component.scss create mode 100644 devui/textarea/demo/count/count.component.ts rename scripts/{es2015-to-es5-bebal-loader => es2015-to-es5-babel-loader}/babel-loader-wepack-config.js (100%) rename scripts/{es2015-to-es5-bebal-loader => es2015-to-es5-babel-loader}/es6-only-third-party-list.js (100%) delete mode 100755 scripts/move-assets.js create mode 100644 src/app/component/scope-list.ts create mode 100644 src/assets/overview/data-entry/datepickerpro.png diff --git a/angular.json b/angular.json index 4d5b489c..a700d025 100755 --- a/angular.json +++ b/angular.json @@ -31,7 +31,8 @@ "assets": [ "src/favicon.ico", "src/assets", - "src/assets/i18n" + "src/assets/i18n", + "src/assets/overview/**/*" ], "styles": [ "node_modules/highlight.js/styles/an-old-hope.css", diff --git a/devui-commons/src/sidebar/sidebar.component.html b/devui-commons/src/sidebar/sidebar.component.html index 46b19a8a..0f3d3de1 100644 --- a/devui-commons/src/sidebar/sidebar.component.html +++ b/devui-commons/src/sidebar/sidebar.component.html @@ -14,6 +14,12 @@ diff --git a/devui-commons/src/sidebar/sidebar.component.scss b/devui-commons/src/sidebar/sidebar.component.scss index d86a0e00..974d3efd 100644 --- a/devui-commons/src/sidebar/sidebar.component.scss +++ b/devui-commons/src/sidebar/sidebar.component.scss @@ -74,4 +74,12 @@ d-accordion ::ng-deep .devui-accordion-menu { box-shadow: none !important; +} + +d-tag { + text-indent: 0; + + & + d-tag { + margin-left: 4px; + } } \ No newline at end of file diff --git a/devui-commons/src/sidebar/sidebar.component.ts b/devui-commons/src/sidebar/sidebar.component.ts index cb519ebc..e5eac372 100644 --- a/devui-commons/src/sidebar/sidebar.component.ts +++ b/devui-commons/src/sidebar/sidebar.component.ts @@ -10,6 +10,10 @@ import { DevuiCommonsService } from '../devui-commons.service'; export class SidebarComponent implements OnInit { @Input() sideMenuList; @Input() linkType = 'routerLink'; + @Input() text = { + new: 'New', + sunset: 'Sunset' + }; _navData; componentsDataDisplay; diff --git a/devui/alert/alert.component.html b/devui/alert/alert.component.html index 2848f6db..ae764f2e 100755 --- a/devui/alert/alert.component.html +++ b/devui/alert/alert.component.html @@ -17,7 +17,7 @@ - + svg path { + fill: $devui-text-weak; + } + } + } + svg.devui-icon { width: 16px; height: 16px; diff --git a/devui/alert/alert.types.ts b/devui/alert/alert.types.ts index 79a11bd5..bf9aaf38 100755 --- a/devui/alert/alert.types.ts +++ b/devui/alert/alert.types.ts @@ -1 +1 @@ -export type AlertType = 'success' | 'danger' | 'warning' | 'info'; +export type AlertType = 'success' | 'danger' | 'warning' | 'info' | 'simple'; diff --git a/devui/alert/demo/basic/basic.component.html b/devui/alert/demo/basic/basic.component.html index c57f5c87..8301c1f9 100755 --- a/devui/alert/demo/basic/basic.component.html +++ b/devui/alert/demo/basic/basic.component.html @@ -2,3 +2,4 @@ danger warning info + simple diff --git a/devui/alert/demo/close/close.component.html b/devui/alert/demo/close/close.component.html index b3c8d5c0..3c5b7585 100755 --- a/devui/alert/demo/close/close.component.html +++ b/devui/alert/demo/close/close.component.html @@ -2,3 +2,4 @@ danger warning info + simple diff --git a/devui/alert/demo/withoutIcon/withoutIcon.component.html b/devui/alert/demo/withoutIcon/withoutIcon.component.html index b517f0f0..172c3160 100644 --- a/devui/alert/demo/withoutIcon/withoutIcon.component.html +++ b/devui/alert/demo/withoutIcon/withoutIcon.component.html @@ -2,3 +2,4 @@ danger warning info + simple diff --git a/devui/alert/doc/api-cn.md b/devui/alert/doc/api-cn.md index d0d40f4c..08a90ad1 100644 --- a/devui/alert/doc/api-cn.md +++ b/devui/alert/doc/api-cn.md @@ -34,5 +34,5 @@ import { AlertModule } from 'ng-devui/alert'; 默认值为'info', 指定alert警告提示的类型 ```ts -export type AlertType = 'success' | 'danger' | 'warning' | 'info'; +export type AlertType = 'success' | 'danger' | 'warning' | 'info' | 'simple'; ``` \ No newline at end of file diff --git a/devui/alert/doc/api-en.md b/devui/alert/doc/api-en.md index e9896f1e..a3af2132 100644 --- a/devui/alert/doc/api-en.md +++ b/devui/alert/doc/api-en.md @@ -34,5 +34,5 @@ In the page: The default value is 'info', which specifies the type of alert warning. ```ts -export type AlertType = 'success' | 'danger' | 'warning' | 'info'; +export type AlertType = 'success' | 'danger' | 'warning' | 'info' | 'simple'; ``` diff --git a/devui/anchor/anchor-link.directive.ts b/devui/anchor/anchor-link.directive.ts index c198e4e6..9353744a 100755 --- a/devui/anchor/anchor-link.directive.ts +++ b/devui/anchor/anchor-link.directive.ts @@ -66,6 +66,9 @@ export class AnchorLinkDirective implements OnInit, OnDestroy { @HostListener('click') scrollToAnchor(activeChangeBy?: AnchorActiveChangeSource) { + if (typeof document === 'undefined') { + return; + } if (!this.anchorBlock) { return; } diff --git a/devui/anchor/anchor.directive.ts b/devui/anchor/anchor.directive.ts index 37e85bf5..e65785b9 100755 --- a/devui/anchor/anchor.directive.ts +++ b/devui/anchor/anchor.directive.ts @@ -122,7 +122,7 @@ export class AnchorDirective implements AfterViewInit, OnDestroy { if (this.scrollListenTarget) { return; } - if (this.boxElement) { + if (this.boxElement && typeof window !== 'undefined') { this.scrollListenTarget = this.boxElement.scrollTarget || window; // window有scroll事件,document.documentElement没有scroll事件 } this.scrollListenTarget.addEventListener('scroll', this.throttle, { passive: true }); diff --git a/devui/anchor/demo/async/async.component.html b/devui/anchor/demo/async/async.component.html index 597ffe24..c29492b0 100644 --- a/devui/anchor/demo/async/async.component.html +++ b/devui/anchor/demo/async/async.component.html @@ -20,19 +20,19 @@
-

Basic Infomation

+

Basic Infomation

Show basic infomation here.
-

Demand List

+

Demand List

Show demand list here.
-

Case List

+

Case List

Show case list here.
-

Quality Assessment

+

Quality Assessment

Show quality assessment here.
diff --git a/devui/anchor/demo/basic/basic.component.html b/devui/anchor/demo/basic/basic.component.html index 52df9a89..9f30b0e8 100755 --- a/devui/anchor/demo/basic/basic.component.html +++ b/devui/anchor/demo/basic/basic.component.html @@ -12,19 +12,19 @@
-

Basic Infomation

+

Basic Infomation

Show basic infomation here.
-

Demand List

+

Demand List

Show demand list here.
-

Case List

+

Case List

Show case list here.
-

Quality Assessment

+

Quality Assessment

Show quality assessment here.
diff --git a/devui/anchor/demo/hash/hash.component.html b/devui/anchor/demo/hash/hash.component.html index e2d21090..432899d9 100644 --- a/devui/anchor/demo/hash/hash.component.html +++ b/devui/anchor/demo/hash/hash.component.html @@ -20,19 +20,19 @@
-

Basic Infomation

+

Basic Infomation

Show basic infomation here.
-

Demand List

+

Demand List

Show demand list here.
-

Case List

+

Case List

Show case list here.
-

Quality Assessment

+

Quality Assessment

Show quality assessment here.
diff --git a/devui/anchor/demo/scroll-target/scroll-target.component.html b/devui/anchor/demo/scroll-target/scroll-target.component.html index 0bfdf8cf..15d86b72 100644 --- a/devui/anchor/demo/scroll-target/scroll-target.component.html +++ b/devui/anchor/demo/scroll-target/scroll-target.component.html @@ -19,19 +19,19 @@
-

Basic Infomation

+

Basic Infomation

Show basic infomation here.
-

Demand List

+

Demand List

Show demand list here.
-

Case List

+

Case List

Show case list here.
-

Quality Assessment

+

Quality Assessment

Show quality assessment here.
diff --git a/devui/back-top/back-top.component.ts b/devui/back-top/back-top.component.ts index d3388cde..49d34bd5 100644 --- a/devui/back-top/back-top.component.ts +++ b/devui/back-top/back-top.component.ts @@ -1,6 +1,7 @@ +import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef + ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef } from '@angular/core'; import { fromEvent, Subscription } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; @@ -25,7 +26,10 @@ export class BackTopComponent implements OnInit, OnChanges, OnDestroy { SCROLL_REFRESH_INTERVAL = 100; target: HTMLElement | Window; subs: Subscription = new Subscription(); - constructor(private cdr: ChangeDetectorRef, private el: ElementRef) {} + document: Document; + constructor(private cdr: ChangeDetectorRef, private el: ElementRef, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } ngOnInit() { this.addScrollEvent(); @@ -64,7 +68,7 @@ export class BackTopComponent implements OnInit, OnChanges, OnDestroy { showButton() { this.currScrollTop = this.target === window ? - (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop) : this.scrollTarget.scrollTop; + (window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop) : this.scrollTarget.scrollTop; if (this.isVisible !== (this.currScrollTop >= this.visibleHeight)) { this.isVisible = !this.isVisible; } @@ -72,8 +76,8 @@ export class BackTopComponent implements OnInit, OnChanges, OnDestroy { goTop() { if (this.target === window) { - document.documentElement.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); - document.body.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); + this.document.documentElement.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); + this.document.body.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); } else { this.scrollTarget.style.scrollBehavior = 'smooth'; this.scrollTarget.scrollTop = 0; diff --git a/devui/back-top/demo/scroll-container/scroll-container.component.ts b/devui/back-top/demo/scroll-container/scroll-container.component.ts index 53240909..11556782 100644 --- a/devui/back-top/demo/scroll-container/scroll-container.component.ts +++ b/devui/back-top/demo/scroll-container/scroll-container.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, OnInit } from '@angular/core'; @Component({ selector: 'd-back-top-scroll-container', @@ -10,13 +11,13 @@ export class ScrollContainerComponent implements OnInit { scrollElement; list = []; sentence = 'You know some birds are not meant to be caged, their feathers are just too bright.'; - constructor() {} + constructor(@Inject(DOCUMENT) private doc: any) {} ngOnInit() { for (let i = 0; i < 20; i++) { this.list.push(this.sentence); } - this.scrollElement = document.querySelector('.devui-scroll-content'); + this.scrollElement = this.doc.querySelector('.devui-scroll-content'); } backTop(event) { diff --git a/devui/button/button.component.scss b/devui/button/button.component.scss index a1dacee4..ac73d23f 100755 --- a/devui/button/button.component.scss +++ b/devui/button/button.component.scss @@ -265,7 +265,7 @@ $devui-btn-pseudo-config: ( &.devui-btn-danger { border-color: $devui-danger; color: $devui-danger; - background-color: $devui-block; + background-color: $devui-danger; } } diff --git a/devui/button/button.component.ts b/devui/button/button.component.ts index ec50c930..10325cf6 100755 --- a/devui/button/button.component.ts +++ b/devui/button/button.component.ts @@ -9,7 +9,7 @@ import { Input, Output, TemplateRef, - ViewChild, + ViewChild } from '@angular/core'; export type IButtonType = 'button' | 'submit' | 'reset'; export type IButtonStyle = 'common' | 'primary' | 'text' | 'text-dark' | 'danger'; @@ -47,7 +47,8 @@ export class ButtonComponent implements AfterContentChecked { } } - constructor(private cd: ChangeDetectorRef) {} + constructor(private cd: ChangeDetectorRef) { + } // 新增click事件,解决直接在host上使用click,在disabled状态下还能触发事件 onClick(event) { diff --git a/devui/button/demo/autofocus/autofocus.component.html b/devui/button/demo/autofocus/autofocus.component.html index 10ae3d10..1cc671c0 100644 --- a/devui/button/demo/autofocus/autofocus.component.html +++ b/devui/button/demo/autofocus/autofocus.component.html @@ -1,2 +1,2 @@ - Confirm + Confirm Cancel diff --git a/devui/button/demo/combination/combination.component.css b/devui/button/demo/combination/combination.component.css index 30321c08..0569cf67 100644 --- a/devui/button/demo/combination/combination.component.css +++ b/devui/button/demo/combination/combination.component.css @@ -1,3 +1,3 @@ d-button:not(:last-child) { - margin-right: 4px; + margin-right: 8px; } diff --git a/devui/button/demo/common/common.component.html b/devui/button/demo/common/common.component.html index 9fe23e4a..6e1a7a61 100755 --- a/devui/button/demo/common/common.component.html +++ b/devui/button/demo/common/common.component.html @@ -1 +1 @@ -Common Disabled +Common Disabled diff --git a/devui/button/demo/groups/groups.component.html b/devui/button/demo/groups/groups.component.html index 5f81ec68..15451862 100644 --- a/devui/button/demo/groups/groups.component.html +++ b/devui/button/demo/groups/groups.component.html @@ -6,8 +6,8 @@ Left - Middle - Right + Middle + Right Filter @@ -21,7 +21,7 @@
New - Delete + Delete
Click me diff --git a/devui/button/demo/icon/icon.component.scss b/devui/button/demo/icon/icon.component.scss index d8a90b8f..cceab545 100644 --- a/devui/button/demo/icon/icon.component.scss +++ b/devui/button/demo/icon/icon.component.scss @@ -5,7 +5,7 @@ } .btn-group d-button { - margin-right: 4px; + margin-right: 8px; } .icon-chevron-down { diff --git a/devui/button/demo/left-right/left-right.component.html b/devui/button/demo/left-right/left-right.component.html index 1ba1ca8e..34e3591f 100644 --- a/devui/button/demo/left-right/left-right.component.html +++ b/devui/button/demo/left-right/left-right.component.html @@ -1,2 +1,2 @@ Left -Right +Right diff --git a/devui/button/demo/primary/primary.component.html b/devui/button/demo/primary/primary.component.html index fadedc76..a5d5fed9 100755 --- a/devui/button/demo/primary/primary.component.html +++ b/devui/button/demo/primary/primary.component.html @@ -1,2 +1,2 @@ -Primary +Primary Disabled diff --git a/devui/button/demo/size/size.component.html b/devui/button/demo/size/size.component.html index eeb02d21..e5a0bf5b 100644 --- a/devui/button/demo/size/size.component.html +++ b/devui/button/demo/size/size.component.html @@ -1,4 +1,4 @@ - Extra small - Small - Middle + Extra small + Small + Middle Large diff --git a/devui/card/demo/basic/basic.component.html b/devui/card/demo/basic/basic.component.html index 2e4f9c9d..a4764d2c 100644 --- a/devui/card/demo/basic/basic.component.html +++ b/devui/card/demo/basic/basic.component.html @@ -1,4 +1,4 @@ - + DEVUI Course diff --git a/devui/card/demo/basic/basic.component.scss b/devui/card/demo/basic/basic.component.scss index e890dfd6..40c2ede6 100644 --- a/devui/card/demo/basic/basic.component.scss +++ b/devui/card/demo/basic/basic.component.scss @@ -16,7 +16,17 @@ } d-card { + cursor: pointer; + transition: + box-shadow $devui-animation-duration-slow $devui-animation-ease-in-smooth, + transform $devui-animation-duration-slow $devui-animation-ease-in-smooth; + &:hover { box-shadow: $devui-shadow-length-hover $devui-light-shadow; + transform: translateY(-5px); } } + +.card-container { + width: 350px; +} diff --git a/devui/card/demo/custom/custom.component.scss b/devui/card/demo/custom/custom.component.scss index bb664c95..824440a1 100644 --- a/devui/card/demo/custom/custom.component.scss +++ b/devui/card/demo/custom/custom.component.scss @@ -15,8 +15,14 @@ } d-card { + cursor: pointer; + transition: + box-shadow $devui-animation-duration-slow $devui-animation-ease-in-smooth, + transform $devui-animation-duration-slow $devui-animation-ease-in-smooth; + &:hover { box-shadow: $devui-shadow-length-hover $devui-light-shadow; + transform: translateY(-5px); } } diff --git a/devui/card/demo/with-media/with-media.component.scss b/devui/card/demo/with-media/with-media.component.scss index 108316c2..ea1befb0 100644 --- a/devui/card/demo/with-media/with-media.component.scss +++ b/devui/card/demo/with-media/with-media.component.scss @@ -22,7 +22,13 @@ } d-card { + cursor: pointer; + transition: + box-shadow $devui-animation-duration-slow $devui-animation-ease-in-smooth, + transform $devui-animation-duration-slow $devui-animation-ease-in-smooth; + &:hover { box-shadow: $devui-shadow-length-hover $devui-light-shadow; + transform: translateY(-5px); } } diff --git a/devui/cascader/cascader-li.component.html b/devui/cascader/cascader-li.component.html index f7d6519a..2e340e2f 100644 --- a/devui/cascader/cascader-li.component.html +++ b/devui/cascader/cascader-li.component.html @@ -15,7 +15,8 @@ diff --git a/devui/cascader/cascader.component.html b/devui/cascader/cascader.component.html index e3ec5b99..7f209d27 100644 --- a/devui/cascader/cascader.component.html +++ b/devui/cascader/cascader.component.html @@ -124,6 +124,10 @@ role="menu" (click)="$event.stopPropagation()" > + + + + ; + @Input() dropdownHeaderTemplate: TemplateRef; @Input() dropdownPanelClass = ''; @Input() @WithConfig() showAnimation = true; @Input() @@ -367,6 +368,9 @@ export class CascaderComponent implements OnInit, OnDestroy, OnChanges, ControlV // 防止位置超出overlay边界 rePosition(): void { + if (typeof window === undefined) { + return; + } setTimeout(() => { this.dropdownComp.reposition(); const width = this.dropdownComp.overlay.overlayRef?.overlayElement.clientWidth; diff --git a/devui/cascader/demo/basic/basic.component.html b/devui/cascader/demo/basic/basic.component.html index 266bcacc..52b4ca5c 100644 --- a/devui/cascader/demo/basic/basic.component.html +++ b/devui/cascader/demo/basic/basic.component.html @@ -5,7 +5,17 @@

hover mode

[(ngModel)]="value1" (ngModelChange)="onChanges($event)" [dropdownPanelClass]="'custom-class'" -> +> + + + + + + + + + +

click mode

+
+
{{ 'components.cascader.header-templateDemo.title' | translate }}
+
+ + + +
diff --git a/devui/cascader/demo/cascader-demo.component.ts b/devui/cascader/demo/cascader-demo.component.ts index 72f7f1e4..221ff4b2 100644 --- a/devui/cascader/demo/cascader-demo.component.ts +++ b/devui/cascader/demo/cascader-demo.component.ts @@ -1,4 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { DevuiSourceData } from 'ng-devui/shared/devui-codebox'; import { TranslateService, TranslationChangeEvent } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; @@ -37,6 +38,11 @@ export class CascaderDemoComponent implements OnInit, OnDestroy { { title: 'TS', language: 'typescript', code: require('!!raw-loader!./parent-select-cascader/parent-select-cascader.component.ts') }, ]; + CascaderDemoHeaderTemplate: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./header-template/cascader-header-template.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./header-template/cascader-header-template.component.ts') }, + { title: 'SCSS', language: 'css', code: require('!!raw-loader!./header-template/cascader-header-template.component.scss') }, + ]; navItems = []; subs: Subscription = new Subscription(); constructor(private translate: TranslateService) {} @@ -64,6 +70,7 @@ export class CascaderDemoComponent implements OnInit, OnDestroy { { dAnchorLink: 'parent-cascader', value: values['parent-cascader'] }, { dAnchorLink: 'template-cascader', value: values['template-cascader'] }, { dAnchorLink: 'lazyload-cascader', value: values['lazyload-cascader'] }, + { dAnchorLink: 'cascader-header-template', value: values['cascader-header-template']}, ]; } diff --git a/devui/cascader/demo/cascader-demo.module.ts b/devui/cascader/demo/cascader-demo.module.ts index cec0140e..2815cc2c 100644 --- a/devui/cascader/demo/cascader-demo.module.ts +++ b/devui/cascader/demo/cascader-demo.module.ts @@ -7,10 +7,12 @@ import { CascaderModule } from 'ng-devui/cascader'; import { DevUIApiComponent } from 'ng-devui/shared/devui-api/devui-api.component'; import { DevUIApiModule } from 'ng-devui/shared/devui-api/devui-api.module'; import { DevUICodeboxModule } from 'ng-devui/shared/devui-codebox'; +import { TabsModule } from 'ng-devui/tabs'; import { TranslateModule } from '@ngx-translate/core'; import { DDemoNavModule } from 'src/app/component/d-demo-nav.module'; import { BasicComponent } from './basic/basic.component'; import { CascaderDemoComponent } from './cascader-demo.component'; +import { CascaderHeaderTemplateComponent } from './header-template/cascader-header-template.component'; import { LazyloadCascaderComponent } from './lazyload-cascader/lazyload-cascader.component'; import { MultipleCascaderComponent } from './multiple-cascader/multiple-cascader.component'; import { ParentSelectCascaderComponent } from './parent-select-cascader/parent-select-cascader.component'; @@ -27,6 +29,7 @@ import { TemplateCascaderComponent } from './template-cascader/template-cascader FormsModule, DDemoNavModule, ButtonModule, + TabsModule, RouterModule.forChild([ { path: '', redirectTo: 'demo' }, { path: 'demo', component: CascaderDemoComponent }, @@ -49,6 +52,7 @@ import { TemplateCascaderComponent } from './template-cascader/template-cascader TemplateCascaderComponent, LazyloadCascaderComponent, ParentSelectCascaderComponent, + CascaderHeaderTemplateComponent, ], }) export class CascaderDemoModule {} diff --git a/devui/cascader/demo/header-template/cascader-header-template.component.html b/devui/cascader/demo/header-template/cascader-header-template.component.html new file mode 100644 index 00000000..4a86fa0b --- /dev/null +++ b/devui/cascader/demo/header-template/cascader-header-template.component.html @@ -0,0 +1,26 @@ + + + + +
+ + + + + +
+
diff --git a/devui/cascader/demo/header-template/cascader-header-template.component.scss b/devui/cascader/demo/header-template/cascader-header-template.component.scss new file mode 100644 index 00000000..84548219 --- /dev/null +++ b/devui/cascader/demo/header-template/cascader-header-template.component.scss @@ -0,0 +1,3 @@ +d-cascader { + display: block; +} diff --git a/devui/cascader/demo/header-template/cascader-header-template.component.ts b/devui/cascader/demo/header-template/cascader-header-template.component.ts new file mode 100644 index 00000000..2bca9f01 --- /dev/null +++ b/devui/cascader/demo/header-template/cascader-header-template.component.ts @@ -0,0 +1,322 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'd-demo-cascader-header-template', + templateUrl: './cascader-header-template.component.html', + styleUrls: ['./cascader-header-template.component.scss'], +}) +export class CascaderHeaderTemplateComponent { + options = [ + { + label: 'option1', + value : 1, + children: [ + { + label: 'option1-1', + value : 4, + children: [ + { + label: 'option1-1-1', + value : 8, + children: [] + }, + { + label: 'option1-1-2', + value : 9, + children: [ + { + label: 'option1-1-2-1', + value : 81, + isLeaf: true + } + ], + } + ] + }, + { + label: 'option1-2', + value : 41, + isLeaf: true + }, + { + label: 'option1-3', + value : 42, + isLeaf: true + }, + { + label: 'option1-4', + value : 43, + isLeaf: true + } + ], + }, + { + label: 'option2', + value : 2, + children: [ + { + label: 'option2-1', + value : 5, + children: [ + { + label: 'option2-1-1', + value : 51, + isLeaf: true + }, + { + label: 'option2-1-2', + value : 61, + isLeaf: true, + disabled: true + } + ] + }, + { + label: 'option2-2', + value : 6, + children: [ + { + label: 'option2-2-1', + value : 512, + isLeaf: true + }, + { + label: 'option2-2-2', + value : 611, + isLeaf: true + } + ] + }, + { + label: 'option2-3', + value : 712, + isLeaf: true + } + ] + }, + { + label: 'option3', + value : 3, + children: [], + isLeaf: true, + disabled: true + } + ]; + + optionMap = { + tab1: [ + { + label: 'option1', + value : 1, + children: [ + { + label: 'option1-1', + value : 4, + children: [ + { + label: 'option1-1-1', + value : 8, + children: [] + }, + { + label: 'option1-1-2', + value : 9, + children: [ + { + label: 'option1-1-2-1', + value : 81, + isLeaf: true + } + ], + } + ] + }, + { + label: 'option1-2', + value : 41, + isLeaf: true + }, + { + label: 'option1-3', + value : 42, + isLeaf: true + }, + { + label: 'option1-4', + value : 43, + isLeaf: true + } + ], + }, + { + label: 'option2', + value : 2, + children: [ + { + label: 'option2-1', + value : 5, + children: [ + { + label: 'option2-1-1', + value : 51, + isLeaf: true + }, + { + label: 'option2-1-2', + value : 61, + isLeaf: true, + disabled: true + } + ] + }, + { + label: 'option2-2', + value : 6, + children: [ + { + label: 'option2-2-1', + value : 512, + isLeaf: true + }, + { + label: 'option2-2-2', + value : 611, + isLeaf: true + } + ] + }, + { + label: 'option2-3', + value : 712, + isLeaf: true + } + ] + }, + { + label: 'option3', + value : 3, + children: [], + isLeaf: true, + disabled: true + } + ], + tab2: [ + { + label: 'group1', + value : 1, + children: [ + { + label: 'group1-1', + value : 4, + children: [ + { + label: 'group1-1-1', + value : 8, + children: [] + }, + { + label: 'group1-1-2', + value : 9, + children: [ + { + label: 'group1-1-2-1', + value : 81, + isLeaf: true + } + ], + } + ] + }, + { + label: 'group1-2', + value : 41, + isLeaf: true + }, + { + label: 'group1-3', + value : 42, + isLeaf: true + }, + { + label: 'group1-4', + value : 43, + isLeaf: true + } + ], + }, + { + label: 'group3', + value : 3, + children: [], + isLeaf: true, + disabled: true + } + ], + tab3: [ + { + label: 'custom1', + value : 1, + children: [ + { + label: 'custom1-1', + value : 4, + children: [ + { + label: 'custom1-1-1', + value : 8, + children: [] + }, + { + label: 'custom1-1-2', + value : 9, + children: [ + { + label: 'custom1-1-2-1', + value : 81, + isLeaf: true + } + ], + } + ] + }, + { + label: 'custom1-2', + value : 41, + isLeaf: true + }, + { + label: 'custom1-3', + value : 42, + isLeaf: true + }, + { + label: 'custom1-4', + value : 43, + isLeaf: true + } + ], + }, + { + label: 'custom3', + value : 3, + children: [], + isLeaf: true, + disabled: true + } + ] + }; + + value1: Array; + + activeTab = 'tab1'; + + onChanges(value: any) { + console.log(value); + } + + activeTabChange(id) { + setTimeout(() => { + this.options = this.optionMap[id]; + }); + } +} diff --git a/devui/cascader/doc/api-cn.md b/devui/cascader/doc/api-cn.md index 5abfe284..5e56879e 100644 --- a/devui/cascader/doc/api-cn.md +++ b/devui/cascader/doc/api-cn.md @@ -16,24 +16,25 @@ import { CascaderModule } from 'ng-devui/cascader'; ## d-cascader 参数 -| 参数 | 类型 | 默认 | 说明 | 跳转 Demo |全局配置项| -| :----------------: | :------------------: | :------------------------------------------------------------------------------: | :------------------------------: | :---------------------------------------------------------------------------------- | ---------------------------------- | -| trigger | `'hover'\|'click'` | 'hover' | 可选,指定展开次级菜单的方式 | [基本用法](demo#basic-usage) | -| options | [`CascaderItem`](#cascaderitem)`[]` | [] | 必选,级联器的菜单信息 | [基本用法](demo#basic-usage) | -| placeholder | `string` | '' | 可选,没有选择时的输入框展示信息 | [基本用法](demo#basic-usage) | -| width | `number` | 200 | 可选,单位 px,用于控制组件输入框宽度和下拉的宽度 | [基本用法](demo#basic-usage) | -| dropdownWidth | `number` | width | 可选,单位 px,控制下拉列表的宽度,默认和组件输入框 width 相等 | [基本用法](demo#basic-usage) | -| disabled | `boolean` | false | 可选,级联器是否禁用 | [基本用法](demo#basic-usage) | -| showPath | `boolean` | false | 可选,级联器选中项是否显示路径,仅单选模式下生效 | [基本用法](demo#basic-usage) | -| allowClear | `boolean` | false | 可选,是否允许清除 | [基本用法](demo#basic-usage) | -| multiple | `boolean` | false | 可选,级联器是否开启多选模式,开启后为 checkbox 选择 | [多选模式](demo#multiple-cascader) | -| canSelectParent | `boolean` | false | 可选,级联器是否允许选择父级节点 | [父级可选](demo#parent-cascader) | +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| :------------------: | :------------------------------------------------------------------------------: | :------------------------------: | :---------------------------------------------------------------------------------: | :--------------------------------- | ---------- | +| trigger | `'hover'\|'click'` | 'hover' | 可选,指定展开次级菜单的方式 | [基本用法](demo#basic-usage) | +| options | [`CascaderItem`](#cascaderitem)`[]` | [] | 必选,级联器的菜单信息 | [基本用法](demo#basic-usage) | +| placeholder | `string` | '' | 可选,没有选择时的输入框展示信息 | [基本用法](demo#basic-usage) | +| width | `number` | 200 | 可选,单位 px,用于控制组件输入框宽度和下拉的宽度 | [基本用法](demo#basic-usage) | +| dropdownWidth | `number` | width | 可选,单位 px,控制下拉列表的宽度,默认和组件输入框 width 相等 | [基本用法](demo#basic-usage) | +| disabled | `boolean` | false | 可选,级联器是否禁用 | [基本用法](demo#basic-usage) | +| showPath | `boolean` | false | 可选,级联器选中项是否显示路径,仅单选模式下生效 | [基本用法](demo#basic-usage) | +| allowClear | `boolean` | false | 可选,是否允许清除 | [基本用法](demo#basic-usage) | +| multiple | `boolean` | false | 可选,级联器是否开启多选模式,开启后为 checkbox 选择 | [多选模式](demo#multiple-cascader) | +| canSelectParent | `boolean` | false | 可选,级联器是否允许选择父级节点 | [父级可选](demo#parent-cascader) | | checkboxRelation | `{upward: boolean, downward: boolean}` | `{upward: true, downward: true}` | 可选,级联器多选下高级状态配置, upward 为状态向父级更新,downward 为状态向子级更新 | [父级可选](demo#parent-cascader) | -| allowSearch | `boolean` | false | 可选,级联器是否开启搜索模式 | [搜索模式](demo#search-cascader) | -| dropDownItemTemplate | `TemplateRef` | false | 可选,传入一个渲染 dropItem 的固定模板 | [模板类型](demo#template-cascader) | -| loadChildrenFn | `(value: CascaderItem) => Promise \| Observable` | null | 可选,传入懒加载的加载子节点的函数 | [点击加载](demo#lazyload-cascader) | -| dropdownPanelClass | `string` | - | 下拉面板的 class,用于用户选中某个面板 | [基本用法](demo#basic-usage) | -| showAnimation | `boolean` | true | 可选,是否开启动画 | | ✔ | +| allowSearch | `boolean` | false | 可选,级联器是否开启搜索模式 | [搜索模式](demo#search-cascader) | +| dropDownItemTemplate | `TemplateRef` | - | 可选,传入一个渲染 dropItem 的固定模板,可获取到option和label参数 | [模板类型](demo#template-cascader) | +| dropdownHeaderTemplate | `TemplateRef` | - | 可选,传入一个渲染下拉头部的渲染模板,可获取到index参数 | [模板类型](demo#cascader-header-template) | +| loadChildrenFn | `(value: CascaderItem) => Promise \| Observable` | null | 可选,传入懒加载的加载子节点的函数 | [点击加载](demo#lazyload-cascader) | +| dropdownPanelClass | `string` | - | 下拉面板的 class,用于用户选中某个面板 | [基本用法](demo#basic-usage) | +| showAnimation | `boolean` | true | 可选,是否开启动画 | | ✔ | ## d-cascader 事件 diff --git a/devui/cascader/doc/api-en.md b/devui/cascader/doc/api-en.md index 2b3678a3..d67baa39 100644 --- a/devui/cascader/doc/api-en.md +++ b/devui/cascader/doc/api-en.md @@ -30,7 +30,8 @@ In the page: | canSelectParent | `boolean` | false | Optional, indicates whether a cascade node can be selected. | [Parent node is optional](demo#parent-cascader) | | checkboxRelation | `{upward: boolean, downward: boolean}` | `{upward: true, downward: true}` | Optional, Advanced status configuration when multiple cascaders are selected. The value up indicates that the status is updated to the parent level, and the value down indicates that the status is updated to the child level. | [Parent optional](demo#parent-cascader) | | allowSearch | `boolean` | false | Optional, Whether to enable the search mode for the cascader. | [Search mode](demo#search-cascader) | -| dropDownItemTemplate | `TemplateRef` | false | Optional, Transfer a fixed template for rendering drop items. | [Template type](demo#template-cascader) | +| dropDownItemTemplate | `TemplateRef` | - | (Optional) Transfer a fixed template for rendering drop items. The options and label parameters can be obtained. | [Template type](demo#template-cascader) | +| dropdownHeaderTemplate | `TemplateRef` | - | (Optional) Input a rendering template of the rendering drop-down header. You can obtain the index parameter | [Template type](demo#cascader-header-template) |. | loadChildrenFn | `(value: CascaderItem) => Promise \| Observable` | null | Optional, Transfer the function for loading subnodes in lazy loading | [Click to load](demo#lazyload-cascader) | | dropdownPanelClass | `string` | - | Class of the drop-down panel, which is used to select a panel. | [Basic usage](demo#basic-usage) | | showAnimation | `boolean` | true | optional. Whether to enable animation. | | ✔ | diff --git a/devui/category-search/category-search.component.html b/devui/category-search/category-search.component.html index 0c42e4e1..870c1744 100644 --- a/devui/category-search/category-search.component.html +++ b/devui/category-search/category-search.component.html @@ -155,7 +155,9 @@ [ngTemplateOutlet]="customGroupNameTemplate || defaultGroupNameTemplate" [ngTemplateOutletContext]="{ tag: item }" > - {{ item?.label }} + {{ + item?.label + }}
{{ i18nCategorySearchText?.noFilterConditions }}
@@ -292,7 +294,7 @@ [dValidateRules]="filterNameRules" /> - -
- {{ i18nCategorySearchText?.confirm }} - {{ i18nCategorySearchText?.cancel }} -
+ + + + +
@@ -445,8 +469,8 @@ [isShowTitle]="true" > - - {{ splitLabel('label', label) }} + + {{ splitLabel('label', label) }} diff --git a/devui/category-search/category-search.component.scss b/devui/category-search/category-search.component.scss index ffb9cfdd..e4f2b66b 100644 --- a/devui/category-search/category-search.component.scss +++ b/devui/category-search/category-search.component.scss @@ -482,3 +482,7 @@ d-tag { padding: 8px 0; text-align: center; } + +.devui-datepicker-footer { + text-align: right; +} diff --git a/devui/category-search/category-search.component.ts b/devui/category-search/category-search.component.ts index e1da269e..751de270 100644 --- a/devui/category-search/category-search.component.ts +++ b/devui/category-search/category-search.component.ts @@ -1,8 +1,10 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, ElementRef, EventEmitter, + Inject, Input, OnChanges, OnDestroy, @@ -14,7 +16,7 @@ import { ViewChild, ViewChildren } from '@angular/core'; -import { DateRangePickerComponent } from 'ng-devui/datepicker'; +import { DatepickerProCalendarComponent } from 'ng-devui/datepicker-pro'; import { DropDownDirective } from 'ng-devui/dropdown'; import { DValidateRules } from 'ng-devui/form'; import { I18nInterface, I18nService } from 'ng-devui/i18n'; @@ -49,7 +51,7 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af @Input() tagMaxWidth: number; @Input() filterNameRules: DValidateRules = []; @Input() beforeTagChange: (tag, searchKey, operation) => boolean | Promise | Observable; - @Input() toggleEvent: (dropdown, tag?) => void; + @Input() toggleEvent: (dropdown, tag?, currentSelectTag?) => void; @Output() searchEvent = new EventEmitter(); @Output() selectedTagsChange = new EventEmitter(); @Output() createFilterEvent = new EventEmitter(); @@ -58,6 +60,8 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af @ViewChild('InputEle') inputEle: ElementRef; @ViewChild('ScrollBarContainer') scrollBarContainer: ElementRef; @ViewChildren('selectedDropdown') selectedDropdownList: QueryList; + @ViewChildren(DatepickerProCalendarComponent) datePickers: QueryList; + @ViewChildren(DatepickerProCalendarComponent, { read: ElementRef }) datePickerElements: QueryList; public currentSelectTag = undefined; public currentTag: ICategorySearchTagItem; public searchField; @@ -80,10 +84,16 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af scrollTimeout: any; // 如果标签在可视范围内则延时展开下拉的定时器 scrollToTailFlag = true; // 是否在更新标签内容后滚动至输入框的开关 DROPDOWN_ANIMATION_TIMEOUT = 200; // 下拉动画延迟 + document: Document; + activeType = 'start'; + get showFilterNameClear() { + return typeof this.filterName === 'string' && this.filterName.length > 0; + } - constructor(private i18n: I18nService, private elm: ElementRef) { + constructor(private i18n: I18nService, private elm: ElementRef, @Inject(DOCUMENT) private doc: any) { this.dateConverter = new DefaultDateConverter(); this.id = CategorySearchComponent.ID_SEED++; + this.document = this.doc; } ngOnInit() { @@ -140,9 +150,9 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af setTagsMaxWidth() { if (this.tagMaxWidth) { const rule = `.devui-category-search-id-${this.id} .devui-tag-container d-tag>.devui-tag-item>span{max-width:${this.tagMaxWidth}px}`; - const style = document.createElement('style'); + const style = this.document.createElement('style'); style.innerText = rule; - document.head.appendChild(style); + this.document.head.appendChild(style); } } @@ -486,19 +496,27 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af this.updateSelectedTags(tag); } - // dateRange 时间范围 处理选中项方法 - getDate(tag, dDatepicker: DateRangePickerComponent) { - if (dDatepicker.rangeStart || dDatepicker.rangeEnd) { - this.afterDropdownClosed(); - const startDate = dDatepicker.rangeStart; - const endDate = dDatepicker.rangeEnd; - tag.value.value = [startDate, endDate]; - tag.value.cache = cloneDeep(tag.value.value); - tag.value[tag.filterKey || 'label'] = tag.showTime - ? this.dateConverter.formatDateTime(startDate) + ' - ' + this.dateConverter.formatDateTime(endDate) - : this.dateConverter.format(startDate) + ' - ' + this.dateConverter.format(endDate); - tag.title = this.setTitle(tag, 'dateRange'); - this.updateSelectedTags(tag); + confirmDate(tag) { + this.afterDropdownClosed(); + this.activeType = 'start'; + tag.value.cache = cloneDeep(tag.value.value); + tag.value[tag.filterKey || 'label'] = tag.showTime + ? this.dateConverter.formatDateTime(tag.value.value[0]) + ' - ' + this.dateConverter.formatDateTime(tag.value.value[1]) + : this.dateConverter.format(tag.value.value[0]) + ' - ' + this.dateConverter.format(tag.value.value[1]); + tag.title = this.setTitle(tag, 'dateRange'); + this.updateSelectedTags(tag); + } + + switchType() { + this.activeType = this.activeType === 'start' ? 'end' : 'start'; + } + + dateValueChange(tag, datepickerpro: DatepickerProCalendarComponent) { + if (this.activeType === 'start') { + tag.value.value ? (tag.value.value[0] = datepickerpro.curActiveDate) : (tag.value.value = [datepickerpro.curActiveDate]); + } + if (this.activeType === 'end') { + tag.value.value[1] = datepickerpro.curActiveDate; } } @@ -527,7 +545,7 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af // checkbox | label 当下拉菜单展开重置多选的选中状态 resetContent(dropdown: DropDownDirective, tag?: any) { if (this.toggleEvent) { - this.toggleEvent(dropdown, tag); + this.toggleEvent(dropdown, tag, this.currentSelectTag); } if (!dropdown.isOpen) { return; @@ -538,6 +556,14 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af tag.value.value = cloneDeep(tag.value.cache); this.handleAccordingType(tag.type, tag.value.options); } + if (tag.type === 'dateRange') { + // 由于datePicker无法初始化显示已有时间,需通过实例调用updateCurPosition方法渲染,因此通过field识别当前显示实例并调用 + const datePickerItem = + this.datePickers.length > 1 + ? this.datePickers.find((v, i) => this.datePickerElements.toArray()[i]?.nativeElement?.id === tag.field) + : this.datePickers.first; + datePickerItem.updateCurPosition(); + } } else if (this.currentSelectTag) { const isArrayType = this.currentSelectTag.type === 'numberRange' || this.currentSelectTag.type === 'treeSelect'; this.currentSelectTag.value.value = isArrayType ? [] : undefined; @@ -553,7 +579,7 @@ export class CategorySearchComponent implements OnInit, OnChanges, OnDestroy, Af break; case 'textInput': setTimeout(() => { - const inputDom: HTMLElement = document.querySelector('.devui-category-search-type-text-input'); + const inputDom: HTMLElement = this.document.querySelector('.devui-category-search-type-text-input'); inputDom?.focus(); }, this.DROPDOWN_ANIMATION_TIMEOUT); break; diff --git a/devui/category-search/category-search.module.ts b/devui/category-search/category-search.module.ts index 7138e088..80142a71 100644 --- a/devui/category-search/category-search.module.ts +++ b/devui/category-search/category-search.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ButtonModule } from 'ng-devui/button'; import { CheckBoxModule } from 'ng-devui/checkbox'; -import { DatepickerModule } from 'ng-devui/datepicker'; +import { DatepickerProModule } from 'ng-devui/datepicker-pro'; import { DropDownModule } from 'ng-devui/dropdown'; import { FormModule } from 'ng-devui/form'; import { InputNumberModule } from 'ng-devui/input-number'; @@ -26,13 +26,13 @@ import { CategorySearchComponent } from './category-search.component'; SearchModule, TagsModule, TagsModule, - DatepickerModule, ButtonModule, CheckBoxModule, InputNumberModule, LoadingModule, TreeModule, PopperModule, + DatepickerProModule ], exports: [CategorySearchComponent], }) diff --git a/devui/category-search/demo/basic/basic.component.ts b/devui/category-search/demo/basic/basic.component.ts index 3cc6a206..11c83e2b 100644 --- a/devui/category-search/demo/basic/basic.component.ts +++ b/devui/category-search/demo/basic/basic.component.ts @@ -68,9 +68,10 @@ export class BasicComponent { console.log('clear all', event); } - toggleEvent(dropdown, tag) { - if (!dropdown.isOpen && tag) { - console.log(tag); + toggleEvent = (dropdown, tag, currentSelectTag) => { + if (dropdown.isOpen) { + console.log('selected and clicked tag:', tag); + console.log('unassigned tag:', currentSelectTag); } } } diff --git a/devui/category-search/doc/api-cn.md b/devui/category-search/doc/api-cn.md index 937677b1..21e96212 100644 --- a/devui/category-search/doc/api-cn.md +++ b/devui/category-search/doc/api-cn.md @@ -14,25 +14,25 @@ import { CategorySearchModule } from 'ng-devui/category-search'; ### d-category-search 参数 -| 参数 | 类型 | 默认 | 说明 | 跳转 | -| :---------------------: | :----------------------: | :---: | :------------------------------------------------------------------------------------------ | :----------------------------------- | -| category | `ICategorySearchTagItem` | -- | 必选,传入分类搜索源数据 | [基本用法](demo#basic-usage) | -| defaultSearchField | `String[]` | [] | 可选,配置输入关键字时可在哪些分类中筛选 | [基本用法](demo#basic-usage) | -| selectedTags | `ICategorySearchTagItem` | -- | 可选,传入需要默认选中的分类数据 | [基本用法](demo#basic-usage) | -| placeholderText | `string` | -- | 可选, 自定义搜索输入框占位文字 | [基本用法](demo#basic-usage) | -| allowSave | `boolean` | true | 可选,是否显示保存当前过滤的按钮 | [基本用法](demo#basic-usage) | -| allowClear | `boolean` | true | 可选,是否显示清除当前过滤的按钮 | [基本用法](demo#basic-usage) | -| allowShowMore | `boolean` | false | 可选,是否显示当前过滤条件下拉列表的按钮 | [大数据量优化展示](demo#auto-scroll) | -| searchKey | `string` | '' | 可选,搜索框内的默认展示值 | [基本用法](demo#basic-usage) | -| beforeTagChange | `function` | -- | 可选,改变标签前调用的方法,返回 boolean 类型,返回 false 可以阻止分类值改变 | | -| toggleEvent | `function` | -- | 可选,已选分类的下拉菜单开关时调用的方法,可使用 return 阻止之后的默认方法执行 | [基本用法](demo#basic-usage) | -| inputReadOnly | `boolean` | false | 可选,限制是否可通过搜索框输入关键字搜索,`true`则无法输入关键字,仅可根据提供的分类数据筛选 | | -| tagMaxWidth | `number` | -- | 可选,单个过滤条件的最大宽度,超过则显示省略号,不设置则不限制 | [大数据量优化展示](demo#auto-scroll) | -| toggleScrollToTail | `boolean` | false | 可选,在有滚动条存在时初始化加载或选择过滤内容后自动滚动至最右侧,以便用户选择新的过滤内容 | [大数据量优化展示](demo#auto-scroll) | -| filterNameRules | `DValidateRules` | false | 可选,配置保存过滤器标题的校验规则,详细规则参见 ng-devui 库 form 组件 | [基本用法](demo#basic-usage) | -| categoryInGroup | `boolean` | false | 可选,是否按组别显示分类下拉列表 | [大数据量优化展示](demo#auto-scroll) | -| groupOrderConfig | `String[]` | -- | 可选,配置组的排序 | [大数据量优化展示](demo#auto-scroll) | -| customGroupNameTemplate | `TemplateRef` | -- | 可选,自定义组名称显示模板 | [大数据量优化展示](demo#auto-scroll) | +| 参数 | 类型 | 默认 | 说明 | 跳转 | +| :---------------------: | :---------------------------------------------------------------------------------: | :---: | :------------------------------------------------------------------------------------------ | :----------------------------------- | +| category | `ICategorySearchTagItem` | -- | 必选,传入分类搜索源数据 | [基本用法](demo#basic-usage) | +| defaultSearchField | `String[]` | [] | 可选,配置输入关键字时可在哪些分类中筛选 | [基本用法](demo#basic-usage) | +| selectedTags | `ICategorySearchTagItem` | -- | 可选,传入需要默认选中的分类数据 | [基本用法](demo#basic-usage) | +| placeholderText | `string` | -- | 可选, 自定义搜索输入框占位文字 | [基本用法](demo#basic-usage) | +| allowSave | `boolean` | true | 可选,是否显示保存当前过滤的按钮 | [基本用法](demo#basic-usage) | +| allowClear | `boolean` | true | 可选,是否显示清除当前过滤的按钮 | [基本用法](demo#basic-usage) | +| allowShowMore | `boolean` | false | 可选,是否显示当前过滤条件下拉列表的按钮 | [大数据量优化展示](demo#auto-scroll) | +| searchKey | `string` | '' | 可选,搜索框内的默认展示值 | [基本用法](demo#basic-usage) | +| beforeTagChange | `(tag, searchKey, operation) => boolean \| Promise \| Observable` | -- | 可选,改变标签前调用的方法,返回 boolean 类型,返回 false 可以阻止分类值改变 | | +| toggleEvent | `(dropdown, tag?, currentSelectTag?) => void` | -- | 可选,已选分类的下拉菜单开关时调用的方法,可使用 return 阻止之后的默认方法执行 | [基本用法](demo#basic-usage) | +| inputReadOnly | `boolean` | false | 可选,限制是否可通过搜索框输入关键字搜索,`true`则无法输入关键字,仅可根据提供的分类数据筛选 | | +| tagMaxWidth | `number` | -- | 可选,单个过滤条件的最大宽度,超过则显示省略号,不设置则不限制 | [大数据量优化展示](demo#auto-scroll) | +| toggleScrollToTail | `boolean` | false | 可选,在有滚动条存在时初始化加载或选择过滤内容后自动滚动至最右侧,以便用户选择新的过滤内容 | [大数据量优化展示](demo#auto-scroll) | +| filterNameRules | `DValidateRules` | false | 可选,配置保存过滤器标题的校验规则,详细规则参见 ng-devui 库 form 组件 | [基本用法](demo#basic-usage) | +| categoryInGroup | `boolean` | false | 可选,是否按组别显示分类下拉列表 | [大数据量优化展示](demo#auto-scroll) | +| groupOrderConfig | `String[]` | -- | 可选,配置组的排序 | [大数据量优化展示](demo#auto-scroll) | +| customGroupNameTemplate | `TemplateRef` | -- | 可选,自定义组名称显示模板 | [大数据量优化展示](demo#auto-scroll) | ### d-category-search 事件 diff --git a/devui/category-search/doc/api-en.md b/devui/category-search/doc/api-en.md index 503e8b13..18bdc4ca 100644 --- a/devui/category-search/doc/api-en.md +++ b/devui/category-search/doc/api-en.md @@ -14,24 +14,24 @@ In the page ### d-category-search 参数 -| Parameter | Type | Default | Description | Jump to Demo | -| :---------------------: | :----------------------: | :-----: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------: | -| category | `ICategorySearchTagItem` | -- | Required. The data of categories. | [Basic usage](demo#basic-usage) | -| defaultSearchField | `String[]` | [] | Optional. Configure the categories that can be filtered when entering keywords. | [Basic usage](demo#basic-usage) | -| selectedTags | `ICategorySearchTagItem` | -- | Optional. The categories to be selected by default. | [Basic usage](demo#basic-usage) | -| allowSave | `boolean` | true | Optional. Whether to show save current filter button. | [Basic usage](demo#basic-usage) | -| allowClear | `boolean` | true | Optional. Whether to display the button for clearing the current filter. | [Basic usage](demo#basic-usage) | -| allowShowMore | `boolean` | false | Optional. Whether to display the button in the drop-down list of the current filter criteria. | [Large-scale data display optimization](demo#auto-scroll) | -| searchKey | `string` | '' | Optional. Default value displayed in the search box. | [Basic usage](demo#basic-usage) | -| beforeTagChange | `function` | -- | Optional. Method called before changing the tag, returns the boolean type, and returns false to prevent the classification value from changing. | | -| toggleEvent | `function` | -- | Optional. Method called when the drop-down menu switch of the selected classification is enabled, which can be executed by the default method after being blocked by return. | [Basic usage](demo#basic-usage) | -| inputReadOnly | `boolean` | false | Optional. Specifies whether to enter keywords in the search box. If it is `true`, you cannot enter keywords and can only filter data based on the provided classification data. | [Basic usage](demo#basic-usage) | -| tagMaxWidth | `number` | -- | Optional. Maximum width of a single filter criterion. If the width exceeds the value, an ellipsis is displayed. If this parameter is not set, no restriction is imposed. | | -| toggleScrollToTail | `boolean` | false | Optional. When a scroll bar exists, the system automatically scrolls to the right after loading or filtering content is selected, so that users can select new filtering content. | [Large-scale data display optimization](demo#auto-scroll) | -| filterNameRules | `DValidateRules` | false | Optional. Configure the validation rule for saving the filter title. For details, see the form component in the ng-devui library. | [Basic usage](demo#basic-usage) | -| categoryInGroup | `boolean` | false | Optional. Indicates whether to display the category drop-down list by group. | [Large-scale data display optimization](demo#auto-scroll) | -| groupOrderConfig | `String[]` | -- | Optional. Configure the sorting of groups. | [Large-scale data display optimization](demo#auto-scroll) | -| customGroupNameTemplate | `TemplateRef` | -- | Optional. Custom Group Name Display Template. | [Large-scale data display optimization](demo#auto-scroll) | +| Parameter | Type | Default | Description | Jump to Demo | +| :---------------------: | :---------------------------------------------------------------------------------: | :-----: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------: | +| category | `ICategorySearchTagItem` | -- | Required. The data of categories. | [Basic usage](demo#basic-usage) | +| defaultSearchField | `String[]` | [] | Optional. Configure the categories that can be filtered when entering keywords. | [Basic usage](demo#basic-usage) | +| selectedTags | `ICategorySearchTagItem` | -- | Optional. The categories to be selected by default. | [Basic usage](demo#basic-usage) | +| allowSave | `boolean` | true | Optional. Whether to show save current filter button. | [Basic usage](demo#basic-usage) | +| allowClear | `boolean` | true | Optional. Whether to display the button for clearing the current filter. | [Basic usage](demo#basic-usage) | +| allowShowMore | `boolean` | false | Optional. Whether to display the button in the drop-down list of the current filter criteria. | [Large-scale data display optimization](demo#auto-scroll) | +| searchKey | `string` | '' | Optional. Default value displayed in the search box. | [Basic usage](demo#basic-usage) | +| beforeTagChange | `(tag, searchKey, operation) => boolean \| Promise \| Observable` | -- | Optional. Method called before changing the tag, returns the boolean type, and returns false to prevent the classification value from changing. | | +| toggleEvent | `(dropdown, tag?, currentSelectTag?) => void` | -- | Optional. Method called when the drop-down menu switch of the selected classification is enabled, which can be executed by the default method after being blocked by return. | [Basic usage](demo#basic-usage) | +| inputReadOnly | `boolean` | false | Optional. Specifies whether to enter keywords in the search box. If it is `true`, you cannot enter keywords and can only filter data based on the provided classification data. | [Basic usage](demo#basic-usage) | +| tagMaxWidth | `number` | -- | Optional. Maximum width of a single filter criterion. If the width exceeds the value, an ellipsis is displayed. If this parameter is not set, no restriction is imposed. | | +| toggleScrollToTail | `boolean` | false | Optional. When a scroll bar exists, the system automatically scrolls to the right after loading or filtering content is selected, so that users can select new filtering content. | [Large-scale data display optimization](demo#auto-scroll) | +| filterNameRules | `DValidateRules` | false | Optional. Configure the validation rule for saving the filter title. For details, see the form component in the ng-devui library. | [Basic usage](demo#basic-usage) | +| categoryInGroup | `boolean` | false | Optional. Indicates whether to display the category drop-down list by group. | [Large-scale data display optimization](demo#auto-scroll) | +| groupOrderConfig | `String[]` | -- | Optional. Configure the sorting of groups. | [Large-scale data display optimization](demo#auto-scroll) | +| customGroupNameTemplate | `TemplateRef` | -- | Optional. Custom Group Name Display Template. | [Large-scale data display optimization](demo#auto-scroll) | ### d-category-search 事件 diff --git a/devui/common/clipboard.directive.ts b/devui/common/clipboard.directive.ts index 32a1398c..adc68248 100644 --- a/devui/common/clipboard.directive.ts +++ b/devui/common/clipboard.directive.ts @@ -1,4 +1,5 @@ import { Clipboard } from '@angular/cdk/clipboard'; +import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, ComponentRef, @@ -6,6 +7,7 @@ import { ElementRef, EventEmitter, HostListener, + Inject, Input, OnDestroy, OnInit, @@ -32,13 +34,17 @@ export class ClipboardDirective implements OnInit , OnDestroy { popoverComponentRef: ComponentRef; i18nCommonText: I18nInterface['common']; i18nSubscription: Subscription; + document: Document; constructor( private elm: ElementRef, private clipboard: Clipboard, private i18n: I18nService, private overlayContainerRef: OverlayContainerRef, - private componentFactoryResolver: ComponentFactoryResolver) {} + private componentFactoryResolver: ComponentFactoryResolver, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } ngOnInit(): void { this.setI18nText(); @@ -54,7 +60,7 @@ export class ClipboardDirective implements OnInit , OnDestroy { @HostListener('click') onClickEvent() { let isSucceeded = false; - const isSupported = !!document.queryCommandSupported && !!document.queryCommandSupported('copy') && !!window; + const isSupported = !!this.document.queryCommandSupported && !!this.document.queryCommandSupported('copy') && !!window; if (isSupported && this.content) { isSucceeded = this.clipboard.copy(this.content); if (isSucceeded) { @@ -82,7 +88,7 @@ export class ClipboardDirective implements OnInit , OnDestroy { appendToBody: true, zIndex: 1060 }); - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); if (!this.sticky) { setTimeout(() => this.destroy(), 3000); } @@ -93,7 +99,7 @@ export class ClipboardDirective implements OnInit , OnDestroy { this.popoverComponentRef.destroy(); this.popoverComponentRef = null; } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } onDocumentClick = (event) => { diff --git a/devui/common/demo/common-demo.component.html b/devui/common/demo/common-demo.component.html index 37f23365..c92f5063 100644 --- a/devui/common/demo/common-demo.component.html +++ b/devui/common/demo/common-demo.component.html @@ -1,12 +1,5 @@
-
-
{{ 'components.common.lazyLoadDemo.title' | translate }}
-
{{ 'components.common.lazyLoadDemo.description' | translate }}
- - - -
{{ 'components.common.pipeDemo.title' | translate }}
@@ -48,4 +41,11 @@
+
+
{{ 'components.common.lazyLoadDemo.title' | translate }}
+
{{ 'components.common.lazyLoadDemo.description' | translate }}
+ + + +
diff --git a/devui/common/demo/common-demo.component.ts b/devui/common/demo/common-demo.component.ts index 4ab78e68..528a898d 100644 --- a/devui/common/demo/common-demo.component.ts +++ b/devui/common/demo/common-demo.component.ts @@ -61,12 +61,12 @@ export class CommonDemoComponent implements OnInit, OnDestroy { setNavValues(values) { this.navItems = [ - { dAnchorLink: 'lazy-load', value: values['lazy-load'] }, { dAnchorLink: 'date-pipe', value: values['date-pipe'] }, { dAnchorLink: 'open-url', value: values['open-url'] }, { dAnchorLink: 'download-file', value: values['download-file'] }, { dAnchorLink: 'iframe-propagate', value: values['iframe-propagate'] }, { dAnchorLink: 'clipboard', value: values['clipboard'] }, + { dAnchorLink: 'lazy-load', value: values['lazy-load'] }, ]; } diff --git a/devui/common/demo/helper-download/helper-download.component.html b/devui/common/demo/helper-download/helper-download.component.html index cbf075cc..222881eb 100644 --- a/devui/common/demo/helper-download/helper-download.component.html +++ b/devui/common/demo/helper-download/helper-download.component.html @@ -1,2 +1,2 @@ Download File -Download File with Header +Download File with Header diff --git a/devui/common/demo/iframe-propagate/iframe-propagate.component.ts b/devui/common/demo/iframe-propagate/iframe-propagate.component.ts index fd6ecac3..0034ce65 100644 --- a/devui/common/demo/iframe-propagate/iframe-propagate.component.ts +++ b/devui/common/demo/iframe-propagate/iframe-propagate.component.ts @@ -1,14 +1,18 @@ -import { AfterViewInit, Component, ElementRef } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Component, ElementRef, Inject } from '@angular/core'; @Component({ selector: 'd-common-iframe-propagate', templateUrl: './iframe-propagate.component.html', }) export class IframPropagateDemoComponent implements AfterViewInit { - constructor(private el: ElementRef) {} + document: Document; + constructor(private el: ElementRef, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } ngAfterViewInit() { - const divElement = document.createElement('div'); + const divElement = this.document.createElement('div'); divElement.innerHTML = `

Child container: iframe

Click iframe to trigger parent's click event, which will change the background color

diff --git a/devui/common/demo/lazy-load/lazy-load.component.html b/devui/common/demo/lazy-load/lazy-load.component.html index bbf6e092..770718d1 100644 --- a/devui/common/demo/lazy-load/lazy-load.component.html +++ b/devui/common/demo/lazy-load/lazy-load.component.html @@ -1,7 +1,16 @@ -
+
+
{{ 'components.common.lazyLoadDemo.p1' | translate }}
  • {{ index + 1 + '. ' + item }}
+
+
{{ 'components.common.lazyLoadDemo.p2' | translate }}
+
    +
  • + {{ index + 1 + '. ' + item }} +
  • +
+
diff --git a/devui/common/demo/lazy-load/lazy-load.component.ts b/devui/common/demo/lazy-load/lazy-load.component.ts index 5242bbb0..4eaf2c42 100644 --- a/devui/common/demo/lazy-load/lazy-load.component.ts +++ b/devui/common/demo/lazy-load/lazy-load.component.ts @@ -12,11 +12,17 @@ export class LazyLoadComponent implements OnInit { next = 1; complete = false; showLoading = false; + + next1 = 1; + showLoading1 = false; + list1 = []; + target = window; constructor() { } ngOnInit() { for (let i = 0; i < 10; i++) { this.list.push(this.sentence); + this.list1.push(this.sentence); } } @@ -38,4 +44,22 @@ export class LazyLoadComponent implements OnInit { this.complete = this.next > this.total; console.log(`load more`, this.next, this.complete); } + onLoadMore1(event) { + if (this.next1 > this.total) { + return; + } + this.showLoading1 = true; + const end = this.next1 + 20; + const tmpList = []; + for (; this.next1 < end; this.next1++) { + tmpList.push(this.sentence); + } + setTimeout(() => { + this.list1 = this.list1.concat(tmpList); + this.showLoading1 = false; + }, 300); + + this.complete = this.next1 > this.total; + console.log(`load more`, this.next, this.complete); + } } diff --git a/devui/common/doc/api-cn.md b/devui/common/doc/api-cn.md index 6c5fa467..4f4af516 100644 --- a/devui/common/doc/api-cn.md +++ b/devui/common/doc/api-cn.md @@ -23,7 +23,7 @@ import { HelperUtils } from 'ng-devui';
-

{{ date | d日期解析器: 'y/MM/dd' }}

+

{{ date | dDatePipe: 'y/MM/dd' }}

@@ -47,6 +47,7 @@ import { HelperUtils } from 'ng-devui'; | 参数 | 类型 | 默认 | 说明 | 跳转 Demo |全局配置项| | :----------------: | :------------: | :-------: | :---: | :------------------: | ---------------------------- | | enableLazyLoad | `boolean` | false | 可选,是否使用懒加载 | [懒加载指令](demo#lazy-load) | +| target | `HTMLElement` | 宿主 | 可选,滚动监听的目标。 | [懒加载指令](demo#lazy-load) | ## dLazyLoad 事件 diff --git a/devui/common/doc/api-en.md b/devui/common/doc/api-en.md index 29880e0e..bff62aec 100644 --- a/devui/common/doc/api-en.md +++ b/devui/common/doc/api-en.md @@ -47,6 +47,7 @@ In the page: | Parameter | Type |Default| Description | Jump to Demo |Global Config| | :----------------: | :------------: | :-------: | :---: | :------------------: | ---------------------------- | | enableLazyLoad | `boolean` | false | Optional. Whether to use lazyload | [Lazyload Directive](demo#lazy-load) | +| target | `HTMLElement` | host | Optional. Indicates the target of the scrolling monitoring. | [懒加载指令](demo#lazy-load) | ## dLazyLoad Event diff --git a/devui/common/helper-utils.ts b/devui/common/helper-utils.ts index 23790b9d..564af486 100755 --- a/devui/common/helper-utils.ts +++ b/devui/common/helper-utils.ts @@ -5,7 +5,7 @@ declare type HttpObserve = 'body' | 'events' | 'response'; declare type ResponseType = 'arraybuffer' | 'blob' | 'json' | 'text'; export class HelperUtils { static jumpOuterUrl(url, target = '_blank') { - if (url !== undefined) { + if (url !== undefined && typeof document !== 'undefined') { const tempLink = document.createElement('a'); tempLink.style.display = 'none'; // for IE 11 tempLink.target = target; @@ -28,6 +28,9 @@ export class HelperUtils { }, onError?: (response) => void ) { + if (typeof document === 'undefined') { + return; + } if (document.querySelector('iframe[name=' + (option && option.iframename || 'download') + ']')) { document.body.removeChild(document.querySelector('iframe[name=' + (option && option.iframename || 'download') + ']')); } @@ -162,6 +165,9 @@ export class HelperUtils { })(responseOption); const downloadFileFromArrayBuffer = (data: ArrayBuffer, filename: string, contentType: string) => { + if (typeof document === 'undefined' || typeof window === 'undefined') { + return; + } if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE11 support const blob = new Blob([data], {type: contentType}); window.navigator.msSaveOrOpenBlob(blob, filename); diff --git a/devui/common/iframe-event-propagate.directive.ts b/devui/common/iframe-event-propagate.directive.ts index 3f36c7ec..60c48bd0 100755 --- a/devui/common/iframe-event-propagate.directive.ts +++ b/devui/common/iframe-event-propagate.directive.ts @@ -1,12 +1,15 @@ -import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Directive, ElementRef, Inject, Input } from '@angular/core'; @Directive({ selector: '[dIframeEventPropagate]' }) export class IframeEventPropagateDirective implements AfterViewInit { @Input() event = 'click'; element: HTMLSelectElement; - constructor(el: ElementRef) { + document: Document; + constructor(el: ElementRef, @Inject(DOCUMENT) private doc: any) { this.element = el.nativeElement; + this.document = this.doc; } ngAfterViewInit() { @@ -34,7 +37,7 @@ export class IframeEventPropagateDirective implements AfterViewInit { } dispatchClickEvent = ($event) => { - const event = document.createEvent('MouseEvents'); + const event = this.document.createEvent('MouseEvents'); event.initEvent(this.event, true, true); event['originEvent'] = $event; this.element.dispatchEvent(event); diff --git a/devui/data-table/data-table-body.component.html b/devui/data-table/data-table-body.component.html index cc6929bf..12101239 100755 --- a/devui/data-table/data-table-body.component.html +++ b/devui/data-table/data-table-body.component.html @@ -66,7 +66,7 @@ [dragOverClass]="rowItem?.$draggable['class']" [dragHandle]="rowItem?.$draggable['handler']" > - + { // wait for table render ready this.renderFakeTable(); this.el.style.display = 'none'; }); - documentElement.addEventListener('resize', this.renderFakeTable); + this.documentElement.addEventListener('resize', this.renderFakeTable); this.createCellMap(); this.scrollViewEl = this.originTable.parentNode.parentNode; this.scrollViewRect = this.scrollViewEl.getBoundingClientRect(); @@ -256,7 +261,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, ngOnDestroy() { if (this.colDraggable) { - documentElement.removeEventListener('resize', this.renderFakeTable); + this.documentElement.removeEventListener('resize', this.renderFakeTable); } if (this.i18nSubscription) { this.i18nSubscription.unsubscribe(); @@ -346,18 +351,18 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, e.preventDefault(); this.originCellIndex = this.findCellIndex(e); setTimeout(() => { // fix chrome bug, mousedown的时候会错误的触发mousemove - documentElement.addEventListener('mousemove', this.handleMousemove); - documentElement.addEventListener('mouseup', () => { - documentElement.removeEventListener('mousemove', this.handleMousemove); + this.documentElement.addEventListener('mousemove', this.handleMousemove); + this.documentElement.addEventListener('mouseup', () => { + this.documentElement.removeEventListener('mousemove', this.handleMousemove); }); }); } handleMousemove = (e) => { e.preventDefault(); - documentElement.removeEventListener('mousemove', this.handleMousemove); - documentElement.addEventListener('mousedown', this.grab); - documentElement.addEventListener('mouseup', this.release); + this.documentElement.removeEventListener('mousemove', this.handleMousemove); + this.documentElement.addEventListener('mousedown', this.grab); + this.documentElement.addEventListener('mouseup', this.release); setTimeout(() => { // fix chrome bug, mousedown的时候会错误的触发mousemove this.dispatchMousedown(); }); @@ -365,11 +370,11 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, grab = (e) => { e.preventDefault(); - documentElement.removeEventListener('mousedown', this.grab); - this.addClass(documentElement, 'gu-unselectable'); + this.documentElement.removeEventListener('mousedown', this.grab); + this.addClass(this.documentElement, 'gu-unselectable'); const context = this.canStart(e.target); this.grabbed = context; - documentElement.addEventListener('mousemove', this.startBecauseMouseMoved); + this.documentElement.addEventListener('mousemove', this.startBecauseMouseMoved); } startBecauseMouseMoved = (e) => { @@ -377,8 +382,8 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, if (!this.grabbed) { return; } - documentElement.removeEventListener('mousemove', this.startBecauseMouseMoved); - documentElement.addEventListener('mousemove', this.drag); + this.documentElement.removeEventListener('mousemove', this.startBecauseMouseMoved); + this.documentElement.addEventListener('mousemove', this.drag); this.source = this.grabbed.source; this.item = this.grabbed.item; this.initialSibling = this.currentSibling = this.nextEl(this.grabbed.item); @@ -400,8 +405,8 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, release = (e) => { e.preventDefault(); this.grabbed = null; - documentElement.removeEventListener('mousemove', this.drag); - documentElement.removeEventListener('mouseup', this.release); + this.documentElement.removeEventListener('mousemove', this.drag); + this.documentElement.removeEventListener('mouseup', this.release); const to = Array.from(this.el.children).indexOf(this.item); this.removeMirrorImage(); this.el.style.display = 'none'; @@ -411,8 +416,8 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, this.fixOriginTable.style.display = 'table'; this.fixFakeTableEl.style.position = 'absolute'; } - documentElement.style.overflow = this.bodyOverflow; - documentElement.removeEventListener('mouseup', this.release); + this.documentElement.style.overflow = this.bodyOverflow; + this.documentElement.removeEventListener('mouseup', this.release); if (this.item) { this.rmClass(this.item, 'gu-transit'); } @@ -476,13 +481,13 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, }; } getScroll(scrollProp, offsetProp) { - if (typeof window[offsetProp] !== 'undefined') { + if (typeof window !== undefined && typeof window[offsetProp] !== 'undefined') { return window[offsetProp]; } - if (documentElement.clientHeight) { - return documentElement[scrollProp]; + if (this.documentElement.clientHeight) { + return this.documentElement[scrollProp]; } - return document.body[scrollProp]; + return this.document.body[scrollProp]; } dispatchMousedown() { @@ -493,9 +498,9 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, getTouchyEvent() { let event; // This is true only for IE,firefox - if (document.createEvent) { + if (this.document.createEvent) { // To create a mouse event , first we need to create an event and then initialize it. - event = document.createEvent('MouseEvent'); + event = this.document.createEvent('MouseEvent'); event.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); } else { event = new MouseEvent('mousedown', { @@ -509,12 +514,12 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, removeMirrorImage() { if (this.mirror) { - documentElement.removeEventListener('mousemove', this.grab); + this.documentElement.removeEventListener('mousemove', this.grab); this.getParent(this.mirror).removeChild(this.mirror); this.mirror = null; setTimeout(() => { this.rmClass(this.mirrorContainer, 'gu-unselectable'); - this.rmClass(documentElement, 'gu-unselectable'); + this.rmClass(this.documentElement, 'gu-unselectable'); }); } } @@ -576,12 +581,12 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, const fakeTable = this.buildFakeTable(this.originTable); this.fakeTable = fakeTable; const el = fakeTable.reduce((previous, current) => { - const li = document.createElement('li'); + const li = this.document.createElement('li'); if (current) { li.appendChild(current); } return previous.appendChild(li) && previous; - }, document.createElement('ul')); + }, this.document.createElement('ul')); this.el = el; this.renderEl(el, this.originTable, fakeTable); } @@ -593,10 +598,10 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, const fakeTable = this.buildFakeTable(this.fixOriginTable); this.mainFakeTable = fakeTable; const el = fakeTable.reduce((previous, current) => { - const li = document.createElement('li'); + const li = this.document.createElement('li'); li.appendChild(current); return previous.appendChild(li) && previous; - }, document.createElement('ul')); + }, this.document.createElement('ul')); this.fixFakeTableEl = el; this.renderEl(el, this.fixOriginTable, this.mainFakeTable); } @@ -609,6 +614,9 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, } renderEl(el, originEl, fakeTables) { + if (typeof window === undefined) { + return; + } this.sizeColumnFake(fakeTables, originEl); this.css(el, { position: 'absolute', @@ -687,7 +695,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, if (this.fixHeader) { const to = Array.from(this.el.children).indexOf(target); const fixTarget = this.fixFakeTableEl.children[to].children[0].cloneNode(true); - const fixTargetContainer = document.createElement('div'); + const fixTargetContainer = this.document.createElement('div'); const mirrorHeight = Math.min(parseInt(this.maxHeight, 10), getFixTableTotalHeight()); fixTargetContainer.style.height = mirrorHeight + 'px'; fixTargetContainer.style.overflow = 'hidden'; @@ -750,7 +758,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, this.animationRequestId = null; } this.handleScroll(clientX, clientY, e); - documentElement.style.overflow = 'hidden'; + this.documentElement.style.overflow = 'hidden'; const x = clientX - this.offsetX; const y = clientY - this.offsetY; @@ -838,7 +846,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, } this.scrollViewEl.scrollTo(scrollLeft, 0); this.animationRequestId = requestAnimationFrame(scrollToLeft); - documentElement.dispatchEvent(e); + this.documentElement.dispatchEvent(e); }; const scrollToRight = () => { @@ -848,7 +856,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, } this.scrollViewEl.scrollTo(scrollLeft, 0); this.animationRequestId = requestAnimationFrame(scrollToRight); - documentElement.dispatchEvent(e); + this.documentElement.dispatchEvent(e); }; if (!this.fixHeader && (y < this.scrollViewRect.top || y > this.scrollViewRect.bottom)) { return; @@ -870,7 +878,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, } getParent(el) { - return el.parentNode === document ? null : el.parentNode; + return el.parentNode === this.document ? null : el.parentNode; } getEventHost(e) { @@ -1055,7 +1063,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, const state = p.className; let el; p.className += ' gu-hide'; - el = document.elementFromPoint(x, y); + el = this.document.elementFromPoint(x, y); p.className = state; return el; } @@ -1158,7 +1166,7 @@ export class DataTableHeadComponent implements OnInit, OnChanges, AfterViewInit, while (immediate !== dropTarget && this.getParent(immediate) !== dropTarget) { immediate = this.getParent(immediate); } - if (immediate === documentElement || isElementDropFreeze(immediate)) { + if (immediate === this.documentElement || isElementDropFreeze(immediate)) { return null; } return immediate; diff --git a/devui/data-table/data-table-row.component.html b/devui/data-table/data-table-row.component.html index ffaba8b4..00050b93 100755 --- a/devui/data-table/data-table-row.component.html +++ b/devui/data-table/data-table-row.component.html @@ -15,7 +15,7 @@ - + g > polygon { :host ::ng-deep .cell-container-inner { position: absolute; max-width: 100%; + width: 100%; &.modify-holder { padding-right: 20px; diff --git a/devui/data-table/data-table.component.ts b/devui/data-table/data-table.component.ts index 9b439bd1..647d3f91 100755 --- a/devui/data-table/data-table.component.ts +++ b/devui/data-table/data-table.component.ts @@ -1,11 +1,12 @@ import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { DOCUMENT } from '@angular/common'; import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, - EventEmitter, HostBinding, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, QueryList, Renderer2, + EventEmitter, HostBinding, Inject, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; import { merge, Subscription } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { switchMap, takeUntil } from 'rxjs/operators'; import { CellSelectedEventArg, CheckableRelation, ColumnResizeEventArg, RowCheckChangeEventArg, RowSelectedEventArg, SortEventArg, TableCheckOptions, TableCheckStatusArg, @@ -318,6 +319,10 @@ export class DataTableComponent implements OnDestroy, OnInit, OnChanges, AfterCo const hasUnChecked = this.dataSource.some(this.hasUnChecked); this._pageAllChecked = dataSource && dataSource.length > 0 && !hasUnChecked; this.halfChecked = hasChecked && hasUnChecked; + + if (this.innerHeader) { + this.innerHeader.setHeaderCheckStatus({pageAllChecked: this._pageAllChecked, pageHalfChecked: this.halfChecked}); + } } get dataSource() { @@ -348,13 +353,15 @@ export class DataTableComponent implements OnDestroy, OnInit, OnChanges, AfterCo } fixHeaderBodyHeight: string; - + document: Document; constructor( private elementRef: ElementRef, private ngZone: NgZone, private renderer: Renderer2, - private cdr: ChangeDetectorRef) { + private cdr: ChangeDetectorRef, + @Inject(DOCUMENT) private doc: any) { this.onDocumentClickListen = this.onDocumentClick.bind(this); + this.document = this.doc; } private getColumns() { @@ -369,7 +376,7 @@ export class DataTableComponent implements OnDestroy, OnInit, OnChanges, AfterCo // life hook start ngOnInit() { this.ngZone.runOutsideAngular(() => { - document.addEventListener('click', this.onDocumentClickListen); + this.document.addEventListener('click', this.onDocumentClickListen); }); } @@ -469,6 +476,12 @@ export class DataTableComponent implements OnDestroy, OnInit, OnChanges, AfterCo } private resetThSortOrder() { + merge(...this.thList?.map(th => th.sortChange)).pipe( + takeUntil(this.thList.changes) + ).subscribe((sortEvent: SortEventArg) => { + this.thList.filter(th => th !== sortEvent.th).forEach(th => th.clearSortOrder()); + }); + this.thList.changes.pipe( switchMap(() => merge(...this.thList.map(th => th.sortChange))) ).subscribe((sortEvent: SortEventArg) => { @@ -497,7 +510,7 @@ export class DataTableComponent implements OnDestroy, OnInit, OnChanges, AfterCo ngOnDestroy(): void { this.unSubscription(); - document.removeEventListener('click', this.onDocumentClickListen); + this.document.removeEventListener('click', this.onDocumentClickListen); } onHandleSort(column: SortEventArg) { diff --git a/devui/data-table/demo/async/data-table-demo-async.component.html b/devui/data-table/demo/async/data-table-demo-async.component.html index 83dcaec3..228904fe 100755 --- a/devui/data-table/demo/async/data-table-demo-async.component.html +++ b/devui/data-table/demo/async/data-table-demo-async.component.html @@ -1,5 +1,5 @@ Request Data -Config Columns +Config Columns

diff --git a/devui/data-table/demo/tree-table/tree-data.component.html b/devui/data-table/demo/tree-table/tree-data.component.html index 0fa673df..0add2777 100755 --- a/devui/data-table/demo/tree-table/tree-data.component.html +++ b/devui/data-table/demo/tree-table/tree-data.component.html @@ -1,7 +1,7 @@ 选中父不再选中子 -选中子不再选中父 -使用自定义展开/收起图标 -展开全部表格 +选中子不再选中父 +使用自定义展开/收起图标 +展开全部表格 g > polygon { .cell-container-inner { position: absolute; max-width: 100%; + width: 100%; min-height: 24px; &.modify-holder { diff --git a/devui/data-table/table/head/th/filter/filter.component.ts b/devui/data-table/table/head/th/filter/filter.component.ts index d97d3b22..fa7ab481 100644 --- a/devui/data-table/table/head/th/filter/filter.component.ts +++ b/devui/data-table/table/head/th/filter/filter.component.ts @@ -1,5 +1,6 @@ +import { DOCUMENT } from '@angular/common'; import { - ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, + ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; import { DropDownDirective } from 'ng-devui/dropdown'; @@ -47,8 +48,11 @@ export class FilterComponent implements OnInit, OnChanges, OnDestroy { i18nCommonText: I18nInterface['common']; filterIconActiveInner: boolean; DEBONCE_TIME = 300; - constructor(private ref: ChangeDetectorRef, private i18n: I18nService, private thComponent: TableThComponent) { + document: Document; + constructor(private ref: ChangeDetectorRef, private i18n: I18nService, private thComponent: TableThComponent, + @Inject(DOCUMENT) private doc: any) { this.i18nCommonText = this.i18n.getI18nText().common; + this.document = this.doc; } ngOnInit() { @@ -160,10 +164,10 @@ export class FilterComponent implements OnInit, OnChanges, OnDestroy { if (this.closeWhenScroll) { const tableViewElement = this.thComponent.tableViewRefElement.nativeElement.querySelector('.devui-scrollbar.scroll-view'); if ($event) { - document.addEventListener('scroll', this.onContainerScroll); + this.document.addEventListener('scroll', this.onContainerScroll); tableViewElement?.addEventListener('scroll', this.onContainerScroll); } else { - document.removeEventListener('scroll', this.onContainerScroll); + this.document.removeEventListener('scroll', this.onContainerScroll); tableViewElement?.removeEventListener('scroll', this.onContainerScroll); } } @@ -224,7 +228,7 @@ export class FilterComponent implements OnInit, OnChanges, OnDestroy { registerFilterChange(): void { this.sourceSubject = new BehaviorSubject(''); setTimeout(() => { - this.searchElement = document.querySelector('.data-table-column-filter-content input'); + this.searchElement = this.document.querySelector('.data-table-column-filter-content input'); this.searchInputValueChangeEvent(); }); this.sourceSubscription = this.sourceSubject.pipe(switchMap(term => this.searchFn(term))).subscribe(options => { diff --git a/devui/data-table/table/head/th/th.component.ts b/devui/data-table/table/head/th/th.component.ts index 7b014562..98b68081 100644 --- a/devui/data-table/table/head/th/th.component.ts +++ b/devui/data-table/table/head/th/th.component.ts @@ -1,6 +1,7 @@ +import { DOCUMENT } from '@angular/common'; import { - ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, - NgZone, OnChanges, OnDestroy, Output, Renderer2, SimpleChanges, TemplateRef + ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Inject, Input, + NgZone, OnChanges, OnDestroy, Output, Renderer2, SimpleChanges, TemplateRef } from '@angular/core'; import { fromEvent, Observable, Subscription } from 'rxjs'; import { FilterConfig, SortDirection, SortEventArg } from '../../../data-table.model'; @@ -74,9 +75,12 @@ export class TableThComponent implements OnChanges, OnDestroy { @Output() tapEvent = new EventEmitter(); @Input() column: any; // 为配置column方式兼容自定义过滤模板context + document: Document; - constructor(element: ElementRef, private renderer2: Renderer2, private zone: NgZone, private cdr: ChangeDetectorRef) { + constructor(element: ElementRef, private renderer2: Renderer2, private zone: NgZone, private cdr: ChangeDetectorRef, + @Inject(DOCUMENT) private doc: any) { this.element = element.nativeElement; + this.document = this.doc; } ngOnChanges(changes: SimpleChanges): void { @@ -214,11 +218,11 @@ export class TableThComponent implements OnChanges, OnDestroy { this.renderer2.addClass(this.element, 'hover-bg'); - const mouseup = fromEvent(document, 'mouseup'); + const mouseup = fromEvent(this.document, 'mouseup'); this.subscription = mouseup.subscribe((ev: MouseEvent) => this.onMouseup(ev)); this.zone.runOutsideAngular(() => { - window.document.addEventListener('mousemove', this.bindMousemove); + this.document.addEventListener('mousemove', this.bindMousemove); }); } } @@ -248,7 +252,7 @@ export class TableThComponent implements OnChanges, OnDestroy { this._destroySubscription(); } - window.document.removeEventListener('mousemove', this.bindMousemove); + this.document.removeEventListener('mousemove', this.bindMousemove); } bindMousemove = (e) => { diff --git a/devui/data-table/table/head/thead.component.ts b/devui/data-table/table/head/thead.component.ts index 3217d471..c6a5270e 100644 --- a/devui/data-table/table/head/thead.component.ts +++ b/devui/data-table/table/head/thead.component.ts @@ -1,4 +1,6 @@ -import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, OnDestroy, OnInit, QueryList } from '@angular/core'; +import { + AfterContentInit, Component, ContentChildren, EventEmitter, Input, OnChanges, OnDestroy, OnInit, QueryList, SimpleChanges +} from '@angular/core'; import { Subscription } from 'rxjs'; import { TableCheckOptions, TableCheckStatusArg } from '../../data-table.model'; import { TableTrComponent } from '../row/tr.component'; @@ -8,7 +10,7 @@ import { TableThComponent } from './th/th.component'; selector: '[dTableHead]', templateUrl: './thead.component.html' }) -export class TableTheadComponent implements OnInit, AfterContentInit, OnDestroy { +export class TableTheadComponent implements OnInit, AfterContentInit, OnDestroy, OnChanges { @Input() checkable: boolean; @Input() checkDisabled: boolean; @Input() checkOptions: TableCheckOptions[]; @@ -48,6 +50,21 @@ export class TableTheadComponent implements OnInit, AfterContentInit, OnDestroy } } + ngOnChanges(changes: SimpleChanges) { + if ( + (changes['checkable'] && !changes['checkable'].isFirstChange()) || + (changes['checkDisabled'] && !changes['checkDisabled'].isFirstChange()) || + (changes['checkOptions'] && !changes['checkOptions'].isFirstChange()) + ) { + if (this.headerFirstRow) { + this.headerFirstRow.headerRowspan = this.headerRowList.length; + this.headerFirstRow.headerCheckable = this.checkable; + this.headerFirstRow.headerCheckDisabled = this.checkDisabled; + this.headerFirstRow.headerCheckOptions = this.checkOptions; + } + } + } + setNestedThToggle() { this.nestedTh = this.thList.find(th => { return th.nestedColumn; diff --git a/devui/datepicker-pro/datepicker-panel.component.html b/devui/datepicker-pro/datepicker-panel.component.html new file mode 100644 index 00000000..6bd40bac --- /dev/null +++ b/devui/datepicker-pro/datepicker-panel.component.html @@ -0,0 +1,41 @@ +
+ + + + +
+ +
+ + +
+ + + +
+ +
+ + +
+ + + + +
+ +
+ + +
+ + + + +
+ +
+ + +
+
diff --git a/devui/datepicker-pro/datepicker-panel.component.scss b/devui/datepicker-pro/datepicker-panel.component.scss new file mode 100644 index 00000000..1a935b9d --- /dev/null +++ b/devui/datepicker-pro/datepicker-panel.component.scss @@ -0,0 +1,15 @@ +@import '../style/theme/color'; + +.devui-custom-panel-wrapper { + display: inline-block; + vertical-align: top; + // width: 150px; + text-align: center; + height: 305px; + font-size: 12px; + border-left: 1px solid $devui-dividing-line; +} + +:host { + font-size: 0; +} diff --git a/devui/datepicker-pro/datepicker-panel.component.ts b/devui/datepicker-pro/datepicker-panel.component.ts new file mode 100644 index 00000000..fa934b14 --- /dev/null +++ b/devui/datepicker-pro/datepicker-panel.component.ts @@ -0,0 +1,27 @@ +import { + Component, HostListener, Input, TemplateRef +} from '@angular/core'; + +@Component({ + selector: 'd-datepicker-panel', + templateUrl: './datepicker-panel.component.html', + styleUrls: ['./datepicker-panel.component.scss'], + preserveWhitespaces: false, +}) +export class DatepickerPanelComponent { + @Input() isRangeType: boolean; + @Input() showTime: boolean; + @Input() showCustom: boolean; + @Input() customTemplate: TemplateRef; + @Input() footerTemplate: TemplateRef; + @Input() mode: 'year' | 'month' | 'date' | 'week' = 'date'; + + constructor() { + } + + @HostListener('click', ['$event']) + onClick(event: MouseEvent) { + event.stopPropagation(); + } + +} diff --git a/devui/datepicker-pro/datepicker-pro-calendar.component.html b/devui/datepicker-pro/datepicker-pro-calendar.component.html new file mode 100644 index 00000000..363b572c --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro-calendar.component.html @@ -0,0 +1,6 @@ + diff --git a/devui/datepicker-pro/datepicker-pro-calendar.component.scss b/devui/datepicker-pro/datepicker-pro-calendar.component.scss new file mode 100644 index 00000000..48bb062e --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro-calendar.component.scss @@ -0,0 +1,3 @@ +:host { + display: inline-block; +} diff --git a/devui/datepicker-pro/datepicker-pro-calendar.component.ts b/devui/datepicker-pro/datepicker-pro-calendar.component.ts new file mode 100644 index 00000000..68a890b3 --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro-calendar.component.ts @@ -0,0 +1,211 @@ +import { + AfterViewInit, + Component, ContentChild, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output, TemplateRef +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DatepickerProService } from './datepicker-pro.service'; + +@Component({ + selector: 'd-datepicker-calendar', + templateUrl: './datepicker-pro-calendar.component.html', + styleUrls: ['./datepicker-pro-calendar.component.scss'], + providers: [ + DatepickerProService, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatepickerProCalendarComponent), + multi: true + } + ], + preserveWhitespaces: false, +}) +export class DatepickerProCalendarComponent implements OnInit, AfterViewInit, OnDestroy { + @Input() isRangeType = false; + @Input() showTime = false; + @Input() mode: 'year' | 'month' | 'date' | 'week' = 'date'; + @Input() startIndexOfWeek = 0; + @Input() set activeRangeType(type: 'start' | 'end') { + this.pickerSrv.currentActiveInput = type; + this.pickerSrv.activeInputChange.next(type); + } + + @Output() confirmEvent = new EventEmitter(); + + @ContentChild('customTemplate') customTemplate: TemplateRef; + @ContentChild('footerTemplate') footerTemplate: TemplateRef; + + get curActiveDate(): Date { + if (this.pickerSrv.currentActiveInput === 'start') { + return this.pickerSrv.curRangeDate[0] || this.pickerSrv.curRangeDate[1] || new Date(); + } else { + return this.pickerSrv.curRangeDate[1] || this.pickerSrv.curRangeDate[0] || new Date(); + } + } + + unsubscribe$ = new Subject(); + + private onChange = (_: any) => null; + private onTouched = () => null; + + constructor( + private pickerSrv: DatepickerProService + ) { + } + + ngOnInit() { + this.initSrvStatus(); + this.initObservable(); + } + + ngAfterViewInit(): void { + this.updateCurPosition(); + } + + public updateCurPosition() { + this.pickerSrv.toggleEvent.next(true); + } + + clear() { + this.pickerSrv.updateDateValue.next({ + type: this.isRangeType ? 'range' : 'single', + value: this.isRangeType ? [] : null + }); + + this.pickerSrv.updateTimeChange.next({ + hour: null, + min: null, + seconds: null + }); + if (this.isRangeType) { + this.pickerSrv.curRangeDate = []; + this.onChange(this.pickerSrv.curRangeDate); + } else { + this.pickerSrv.curDate = null; + this.onChange(this.pickerSrv.curDate); + } + } + + private initSrvStatus() { + this.pickerSrv.showTime = this.showTime; + this.pickerSrv.isRange = this.isRangeType; + this.pickerSrv.startIndexOfWeek = this.startIndexOfWeek; + } + + private initObservable() { + this.pickerSrv.selectedDateChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(change => { + if (this.isRangeType) { + this.pickerSrv.curRangeDate = change.value as Date[]; + this.onChange(this.pickerSrv.curRangeDate); + } else { + this.pickerSrv.curDate = change.value as Date; + this.onChange(change.value); + } + + }); + + this.pickerSrv.selectedTimeChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(time => { + if (this.isRangeType) { + const curTime = new Date(this.curActiveDate.getTime()).setHours(time.hour, time.min, time.seconds); + const curDate = new Date(curTime); + if (time.activeInput === 'start') { + this.pickerSrv.curRangeDate[0] = curDate; + if (this.isSameDateAndTimeWrong()) { + this.pickerSrv.curRangeDate[1] = curDate; + } + } else { + this.pickerSrv.curRangeDate[1] = curDate; + if (this.isSameDateAndTimeWrong()) { + this.pickerSrv.curRangeDate[0] = curDate; + } + } + this.onChange(this.pickerSrv.curRangeDate); + } else { + const curDate = new Date((this.pickerSrv.curDate || new Date()).setHours(time.hour, time.min, time.seconds)); + this.pickerSrv.curDate = curDate; + this.onChange(curDate); + } + + }); + + this.pickerSrv.closeDropdownEvent.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(isConfirm => { + if (isConfirm) { + this.confirmEvent.emit(this.pickerSrv.curDate || this.pickerSrv.curRangeDate); + } + }); + } + + isSameDateAndTimeWrong(): boolean { + if (this.pickerSrv.curRangeDate[0]?.toDateString() === this.pickerSrv.curRangeDate[1]?.toDateString()) { + return this.pickerSrv.curRangeDate[0].getTime() > this.pickerSrv.curRangeDate[1].getTime(); + } + return false; + } + + writeValue(value: Date | Date[]) { + if (this.isRangeType) { + this.writeRangeValue(value as Date[]); + } else { + this.writeSingleValue(value as Date); + } + } + + writeRangeValue(value: Date[]) { + if (!value || !value.length) { + return; + } + + if (value.find(t => !this.pickerSrv.dateInRange(t))) { + return; + } + + this.pickerSrv.curRangeDate = value; + this.pickerSrv.updateDateValue.next({ + type: 'range', + value + }); + } + + writeSingleValue(value: Date) { + if (!value) { + return; + } + if (!this.pickerSrv.dateInRange(new Date(value))) { + this.clear(); + return; + } + this.pickerSrv.curDate = value; + this.pickerSrv.updateDateValue.next({ + type: 'single', + value + }); + + if (this.showTime) { + this.pickerSrv.updateTimeChange.next({ + hour: value.getHours(), + min: value.getMinutes(), + seconds: value.getSeconds() + }); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } +} diff --git a/devui/datepicker-pro/datepicker-pro.component.html b/devui/datepicker-pro/datepicker-pro.component.html new file mode 100644 index 00000000..63f3e4f5 --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro.component.html @@ -0,0 +1,59 @@ +
+
+ +
+ +
+ + + + + + + + +
+ +
+
+
+ +
+ + +
+
diff --git a/devui/datepicker-pro/datepicker-pro.component.scss b/devui/datepicker-pro/datepicker-pro.component.scss new file mode 100644 index 00000000..5ad6a833 --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro.component.scss @@ -0,0 +1,46 @@ +@import '../style/theme/color'; + +.devui-datepicker-pro-wrapper { + display: inline-block; + background-color: $devui-base-bg; + + .devui-single-picker { + padding-left: 8px; + position: relative; + box-sizing: border-box; + width: 100%; + min-height: 24px; + + .devui-input { + height: 24px; + padding: 4px 10px; + width: calc(100% - 16px); + } + + &-icon { + vertical-align: bottom; + } + + .close-icon-wrapper { + padding: 0 8px; + vertical-align: baseline; + visibility: hidden; + } + + &.devui-has-value:hover:not(.devui-disabled) .close-icon-wrapper { + visibility: visible; + } + + &:not(.devui-disabled) .close-icon-wrapper { + cursor: pointer; + } + } + + .devui-input-group > .devui-input { + display: inline-block; + + &::-ms-clear { + display: none; + } + } +} diff --git a/devui/datepicker-pro/datepicker-pro.component.ts b/devui/datepicker-pro/datepicker-pro.component.ts new file mode 100644 index 00000000..0c34eeed --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro.component.ts @@ -0,0 +1,302 @@ +import { + AfterViewInit, + Component, + ContentChild, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { I18nInterface, I18nService } from 'ng-devui/i18n'; +import { DefaultDateConverter } from 'ng-devui/utils'; +import { fromEvent, Subject } from 'rxjs'; +import { debounceTime, takeUntil } from 'rxjs/operators'; +import { DatepickerProService } from './datepicker-pro.service'; +import { DateConfig } from './lib/datepicker-pro.type'; + +@Component({ + selector: 'd-datepicker-pro', + templateUrl: './datepicker-pro.component.html', + styleUrls: ['./datepicker-pro.component.scss'], + providers: [ + DatepickerProService, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatepickerProComponent), + multi: true + } + ], + preserveWhitespaces: false, +}) +export class DatepickerProComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { + @Input() mode: 'year' | 'month' | 'date' = 'date'; + @Input() showTime = false; + @Input() disabled = false; + @Input() autoOpen = false; + @Input() format: string; + @Input() cssClass: string; + @Input() showAnimation = true; + @Input() width: string; + @Output() dropdownToggle = new EventEmitter(); + @Output() confirmEvent = new EventEmitter(); + @Input() set calenderRange (value) { + this.pickerSrv.calendarRange = value || [1970, 2099]; + } + @Input() set minDate(value: Date) { + this.pickerSrv.minDate = value; + } + @Input() set maxDate(value: Date) { + this.pickerSrv.maxDate = value; + } + @ContentChild('customTemplate') customTemplate: TemplateRef; + @ContentChild('footerTemplate') footerTemplate: TemplateRef; + @ContentChild('hostTemplate') hostTemplate: TemplateRef; + @ViewChild('dateInput') datepickerInput: ElementRef; + + private i18nLocale: I18nInterface['locale']; + + i18nText; + dateValue = ''; + datepickerConvert: DefaultDateConverter; + unsubscribe$ = new Subject(); + isOpen = false; + get dateConfig(): DateConfig { + return { + dateConverter: this.datepickerConvert, + min: this.pickerSrv.minDate || new Date(this.pickerSrv.calendarRange[0] + '/01/01'), + max: this.pickerSrv.maxDate || new Date(this.pickerSrv.calendarRange[1] + '/12/31'), + format: { + date: this.format || 'y/MM/dd', + time: this.format || 'y/MM/dd HH:mm:ss', + month: this.format || 'y-MM', + year: 'y' + } + }; + } + + get curFormat(): string { + if (this.mode === 'year') { + return this.dateConfig.format.year; + } else if (this.mode === 'month') { + return this.dateConfig.format.month; + } else { + return this.showTime ? this.dateConfig.format.time : this.dateConfig.format.date; + } + } + + private onChange = (_: any) => null; + private onTouched = () => null; + + constructor( + private i18n: I18nService, + private pickerSrv: DatepickerProService + ) { + this.i18nText = this.i18n.getI18nText().datePickerPro; + this.datepickerConvert = new DefaultDateConverter(); + } + + ngOnInit() { + this.initSrvStatus(); + this.setI18nText(); + setTimeout(() => { + this.isOpen = this.autoOpen; + }); + } + + ngAfterViewInit(): void { + this.initObservable(); + } + + private initSrvStatus() { + this.pickerSrv.showTime = this.showTime; + this.pickerSrv.isRange = false; + } + + private initObservable() { + this.pickerSrv.selectedDateChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(change => { + this.dateValue = this.formatDateToString(change.value as Date); + this.pickerSrv.curDate = change.value as Date; + this.onChange(change.value); + }); + + this.pickerSrv.selectedTimeChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(time => { + if (this.dateValue) { + const curTime = this.datepickerConvert.parse(this.dateValue).setHours(time.hour, time.min, time.seconds); + const curDate = new Date(curTime); + this.pickerSrv.curDate = curDate; + this.dateValue = this.formatDateToString(curDate); + this.onChange(curDate); + } else { + this.writeValue(new Date(new Date().setHours(time.hour, time.min, time.seconds))); + this.onChange(this.pickerSrv.curDate); + } + }); + + this.pickerSrv.closeDropdownEvent.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(isConfirm => { + this.isOpen = false; + this.dropdownToggle.emit(false); + if (isConfirm) { + this.confirmEvent.emit(this.pickerSrv.curDate); + } + }); + + if (!this.hostTemplate) { + fromEvent(this.datepickerInput.nativeElement, 'input').pipe( + takeUntil(this.unsubscribe$), + debounceTime(300) + ).subscribe((event: InputEvent) => { + if (!this.dateValue) { + return; + } + + const inputDate = this.datepickerConvert.parse(this.dateValue, this.curFormat); + if (inputDate instanceof Date && inputDate.getTime() === this.pickerSrv.curDate.getTime()) { + return; + } + + if (this.validateDate(this.dateValue)) { + this.pickerSrv.curDate = inputDate; + this.pickerSrv.updateDateValue.next({ + type: 'single', + value: inputDate + }); + + if (this.showTime) { + this.pickerSrv.updateTimeChange.next({ + hour: inputDate.getHours(), + min: inputDate.getMinutes(), + seconds: inputDate.getSeconds() + }); + } + } + }); + + fromEvent(this.datepickerInput.nativeElement, 'blur').pipe( + takeUntil(this.unsubscribe$), + ).subscribe(() => { + if (!this.validateDate(this.dateValue)) { + this.dateValue = this.pickerSrv.curDate ? + this.datepickerConvert.format(this.pickerSrv.curDate, this.curFormat, this.i18nLocale) : + ''; + } + }); + } + } + + private setI18nText() { + this.i18nLocale = this.i18n.getI18nText().locale; + this.i18n.langChange().pipe( + takeUntil(this.unsubscribe$) + ).subscribe((data) => { + this.i18nLocale = data.locale; + this.i18nText = data.datePickerPro; + }); + } + + validateDate(value: string) { + const valueDate = this.datepickerConvert.parse(value, this.curFormat); + const valueFormat = valueDate && !isNaN(valueDate.getTime()) && + this.datepickerConvert.format(valueDate, this.curFormat, this.i18nLocale); + if ( + !valueDate || value !== valueFormat || + (value === valueFormat && !this.pickerSrv.dateInRange(valueDate)) + ) { + return false; + } else { + return true; + } + } + + formatDateToString(date: Date): string { + return this.datepickerConvert.format(date, this.curFormat); + } + + clear(event?: MouseEvent) { + event?.stopPropagation(); + if (this.disabled) { + return; + } + this.pickerSrv.updateDateValue.next({ + type: 'single', + value: null + }); + + this.pickerSrv.updateTimeChange.next({ + hour: null, + min: null, + seconds: null + }); + this.dateValue = null; + this.pickerSrv.curDate = null; + this.onChange(this.pickerSrv.curDate); + } + + onToggle(isOpen) { + if (isOpen !== this.isOpen || isOpen) { + this.dropdownToggle.emit(isOpen); + } + this.isOpen = isOpen; + this.pickerSrv.toggleEvent.next(isOpen); + } + + openDropdown(event: Event) { + if (this.isOpen) { + event.stopPropagation(); + } + this.isOpen = true; + + setTimeout(() => { + this.datepickerInput?.nativeElement?.focus(); + }); + } + + writeValue(value: Date) { + if (!value) { + return; + } + if (!this.pickerSrv.dateInRange(new Date(value))) { + this.clear(); + return; + } + this.dateValue = this.formatDateToString(value); + this.pickerSrv.curDate = value; + this.pickerSrv.updateDateValue.next({ + type: 'single', + value + }); + + if (this.showTime) { + this.pickerSrv.updateTimeChange.next({ + hour: value.getHours(), + min: value.getMinutes(), + seconds: value.getSeconds() + }); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + +} diff --git a/devui/datepicker-pro/datepicker-pro.module.ts b/devui/datepicker-pro/datepicker-pro.module.ts new file mode 100644 index 00000000..eac31c28 --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro.module.ts @@ -0,0 +1,48 @@ +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ButtonModule } from 'ng-devui/button'; +import { DropDownModule } from 'ng-devui/dropdown'; +import { DatepickerPanelComponent } from './datepicker-panel.component'; +import { DatepickerProCalendarComponent } from './datepicker-pro-calendar.component'; +import { DatepickerProComponent } from './datepicker-pro.component'; +import { DatepickerProCommonDataService } from './datepicker-pro.service'; +import { CalendarPanelComponent } from './lib/calendar-panel.component'; +import { FooterPanelComponent } from './lib/footer-panel.component'; +import { MonthPanelComponent } from './lib/month-panel.component'; +import { TimepickerPanelComponent } from './lib/timepicker-panel.component'; +import { YearPanelComponent } from './lib/year-panel.component'; +import { RangeDatepickerProComponent } from './range-datepicker-pro.component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + DropDownModule, + ScrollingModule, + ButtonModule + ], + exports: [ + DatepickerProComponent, + DatepickerPanelComponent, + RangeDatepickerProComponent, + DatepickerProCalendarComponent + ], + declarations: [ + DatepickerProComponent, + DatepickerPanelComponent, + TimepickerPanelComponent, + FooterPanelComponent, + CalendarPanelComponent, + MonthPanelComponent, + YearPanelComponent, + RangeDatepickerProComponent, + DatepickerProCalendarComponent + ], + providers: [ + DatepickerProCommonDataService + ] +}) +export class DatepickerProModule { +} diff --git a/devui/datepicker-pro/datepicker-pro.service.ts b/devui/datepicker-pro/datepicker-pro.service.ts new file mode 100644 index 00000000..16631ab3 --- /dev/null +++ b/devui/datepicker-pro/datepicker-pro.service.ts @@ -0,0 +1,250 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable() +export class DatepickerProService implements OnDestroy { + curDate: Date; + curRangeDate: Date[] = []; + curHoverDate: Date; + startIndexOfWeek: number; + isRange: boolean; + showTime: boolean; + calendarRange = [1970, 2099]; + currentActiveInput: 'start' | 'end' = 'start'; + _minDate: Date = new Date(this.calendarRange[0], 0, 1); + _maxDate: Date = new Date(this.calendarRange[1], 11, 31); + document: Document; + + set minDate(value: Date) { + this._minDate = new Date(value) || new Date(this.calendarRange[0], 0, 1); + } + set maxDate(value: Date) { + this._maxDate = new Date(value) || new Date(this.calendarRange[1], 11, 31); + } + + get minDate(): Date { + return this._minDate; + } + get maxDate(): Date { + return this._maxDate; + } + get closeAfterSelected(): boolean { + return !this.isRange && !this.showTime; + } + + get curHour() { + if (this.isRange) { + return (this.currentActiveInput === 'start' ? this.curRangeDate[0]?.getHours() : this.curRangeDate[1]?.getHours()) || 0; + } else { + return this.curDate?.getHours() || 0; + } + } + get curMin() { + if (this.isRange) { + return (this.currentActiveInput === 'start' ? this.curRangeDate[0]?.getMinutes() : this.curRangeDate[1]?.getMinutes()) || 0; + } else { + return this.curDate?.getMinutes() || 0; + } + } + get curSec() { + if (this.isRange) { + return (this.currentActiveInput === 'start' ? this.curRangeDate[0]?.getSeconds() : this.curRangeDate[1]?.getSeconds()) || 0; + } else { + return this.curDate?.getSeconds() || 0; + } + } + readonly toggleEvent = new Subject(); + readonly closeDropdownEvent = new Subject(); + readonly activeInputChange = new Subject<'start' | 'end'>(); + readonly selectedDateChange = new Subject<{ + type: 'single' | 'range'; + value: Date | Date[]; + }>(); + readonly updateDateValue = new Subject<{ + type: 'single' | 'range'; + value: Date | Date[]; + }>(); + readonly selectedTimeChange = new Subject<{ + activeInput?: 'start' | 'end', + hour: number; + min: number; + seconds: number; + }>(); + readonly updateTimeChange = new Subject<{ + activeInput?: 'start' | 'end', + hour: number; + min: number; + seconds: number; + }>(); + + constructor(@Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } + dateInRange(date: Date): boolean { + if (!date) { + return true; + } + return (date.getTime() > this.minDate.getTime() && date.getTime() < this.maxDate.getTime()) || + date.toDateString() === this.minDate.toDateString() || date.toDateString() === this.maxDate.toDateString(); + } + + // 对范围模式下一些非法的选择进行修正 + fixRangeDate() { + const start = this.curRangeDate[0]?.getTime(); + const end = this.curRangeDate[1]?.getTime(); + + if (start && end && end < start) { + if (this.currentActiveInput === 'start') { + this.curRangeDate[1] = null; + } else if (this.currentActiveInput === 'end') { + this.curRangeDate[0] = null; + } + } + } + + // 判断日期是否为起始日期 + isStartDate(date: Date): boolean { + if (!this.isRange) { + return false; + } + + if (this.currentActiveInput === 'start') { + return date.toDateString() === this.curHoverDate?.toDateString() || date.toDateString() === this.curRangeDate[0]?.toDateString(); + } + + return date.toDateString() === this.curRangeDate[0]?.toDateString(); + } + + // 判断日期是否为结束日期 + isEndDate(date: Date): boolean { + if (!this.isRange) { + return false; + } + + if (this.currentActiveInput === 'end') { + return date.toDateString() === this.curHoverDate?.toDateString() || date.toDateString() === this.curRangeDate[1]?.toDateString(); + } + + return date.toDateString() === this.curRangeDate[1]?.toDateString(); + } + + // 判断日期是否在hover范围或者选中的范围内 + isDateInRange(date: Date): boolean { + const dateTime = date.getTime(); + const dateStr = date.toDateString(); + if (this.isRange) { + if (this.currentActiveInput === 'start') { + return (this.curHoverDate || this.curRangeDate[0])?.getTime() < dateTime && + (this.curHoverDate || this.curRangeDate[0])?.toDateString() !== dateStr && + this.curRangeDate[1]?.getTime() > dateTime && + this.curRangeDate[1]?.toDateString() !== dateStr; + } else { + return this.curRangeDate[0]?.getTime() < dateTime && + this.curRangeDate[0]?.toDateString() !== dateStr && + (this.curHoverDate || this.curRangeDate[1])?.getTime() > dateTime && + (this.curHoverDate || this.curRangeDate[1]).toDateString() !== dateStr; + } + } else { + return false; + } + } + + // 判断日期是否在已选中的范围内,与hover做区分 + isDateInSelectRange(date: Date) { + if (!this.isRange) { + return false; + } + + if (!this.curRangeDate[0] || !this.curRangeDate[1]) { + return false; + } + + return this.curRangeDate[0].getTime() < date.getTime() && this.curRangeDate[1].getTime() > date.getTime() && + this.curRangeDate[1].toDateString() !== date.toDateString() && this.curRangeDate[0].toDateString() !== date.toDateString(); + } + + isDateActive(date: Date): boolean { + const dateStr = date.toDateString(); + if (this.isRange) { + return dateStr === this.curRangeDate[0]?.toDateString() || dateStr === this.curRangeDate[1]?.toDateString(); + } else { + return dateStr === this.curDate?.toDateString(); + } + } + + isMonthActive(yearIndex: number, monthIndex: number): boolean { + if (this.isRange) { + return (yearIndex === this.curRangeDate[0]?.getFullYear() && monthIndex === this.curRangeDate[0]?.getMonth()) || + (yearIndex === this.curRangeDate[1]?.getFullYear() && monthIndex === this.curRangeDate[1]?.getMonth()); + } else { + return yearIndex === this.curDate?.getFullYear() && monthIndex === this.curDate?.getMonth(); + } + } + + isYearActive(yearIndex: number): boolean { + if (this.isRange) { + return yearIndex === this.curRangeDate[0]?.getFullYear() || yearIndex === this.curRangeDate[1]?.getFullYear(); + } else { + return yearIndex === this.curDate?.getFullYear(); + } + } + + // 是否为范围选中日期中对应的input激活项 + isActiveInputTypeDate(date: Date) { + if (!this.isRange) { + return false; + } + + if (this.currentActiveInput === 'start') { + return date.toDateString() === this.curRangeDate[0]?.toDateString(); + } else { + return date.toDateString() === this.curRangeDate[1]?.toDateString(); + } + } + + // 是否为选中日期且在废弃范围逻辑内 + isDateAbandon(date: Date): boolean { + if (!this.isRange || (!this.curRangeDate[0] || !this.curRangeDate[1])) { + return false; + } + + if (!this.isDateActive(date)) { + return false; + } + + if (this.currentActiveInput === 'start') { + return this.curHoverDate?.getTime() > date.getTime(); + } else { + return this.curHoverDate?.getTime() < date.getTime(); + } + } + + mearsureStrWidth(str: string): number { + const mearsureDom = this.document.createElement('span'); + mearsureDom.innerText = str; + mearsureDom.style.visibility = 'hidden'; + this.document.body.appendChild(mearsureDom); + + const domWidth = mearsureDom.offsetWidth; + + this.document.body.removeChild(mearsureDom); + return domWidth; + } + + ngOnDestroy(): void { + this.toggleEvent.complete(); + this.selectedDateChange.complete(); + this.closeDropdownEvent.complete(); + this.updateDateValue.complete(); + this.updateTimeChange.complete(); + this.selectedTimeChange.complete(); + this.activeInputChange.complete(); + } + +} + +@Injectable() +export class DatepickerProCommonDataService { + calendarDataCache = {}; +} diff --git a/devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.html b/devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.html new file mode 100644 index 00000000..53407a15 --- /dev/null +++ b/devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.html @@ -0,0 +1,13 @@ + + +
format
+ + + +
limit range
+ + + +
disable
+ + diff --git a/devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.ts b/devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.ts new file mode 100644 index 00000000..157ab016 --- /dev/null +++ b/devui/datepicker-pro/demo/basic/basic-datepicker-pro.component.ts @@ -0,0 +1,19 @@ +import { + Component +} from '@angular/core'; + +@Component({ +selector: 'd-basic-datepicker-pro', +templateUrl: './basic-datepicker-pro.component.html', +}) +export class BasicDatepickerProComponent { + value1 = new Date(); + value2 = new Date(); + value3 = new Date(); + value4 = new Date('2021/04/01'); + minDate = new Date(new Date().setMonth(new Date().getMonth() - 1)); + maxDate = new Date(new Date().setMonth(new Date().getMonth() + 2)); + constructor() { + } + +} diff --git a/devui/datepicker-pro/demo/datepicker-pro-demo.component.html b/devui/datepicker-pro/demo/datepicker-pro-demo.component.html new file mode 100644 index 00000000..00ee93f7 --- /dev/null +++ b/devui/datepicker-pro/demo/datepicker-pro-demo.component.html @@ -0,0 +1,67 @@ +
+ +
+
{{ 'components.datepicker-pro.basicDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.showTimeDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.templateDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.monthYearDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.rangePickerDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.rangeTemplateDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.hostTemplateDemo.title' | translate }}
+ + + +
+ +
+
{{ 'components.datepicker-pro.static-panelDemo.title' | translate }}
+
+ + + +
+ +
+
{{ 'components.datepicker-pro.select-type.title' | translate }}
+
+ + + +
+
diff --git a/devui/datepicker-pro/demo/datepicker-pro-demo.component.ts b/devui/datepicker-pro/demo/datepicker-pro-demo.component.ts new file mode 100644 index 00000000..7235e268 --- /dev/null +++ b/devui/datepicker-pro/demo/datepicker-pro-demo.component.ts @@ -0,0 +1,96 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { DevuiSourceData } from 'ng-devui/shared/devui-codebox/devui-source-data'; +import { TranslateService, TranslationChangeEvent } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +@Component({ + selector: 'd-datepicker-pro-demo', + templateUrl: './datepicker-pro-demo.component.html', +}) +export class DatepickerProDemoComponent implements OnInit, OnDestroy { + BasicSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./basic/basic-datepicker-pro.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./basic/basic-datepicker-pro.component.ts') }, + ]; + + showTimeSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./show-time/show-time-picker.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./show-time/show-time-picker.component.ts') }, + ]; + + templateSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./template/datepicker-template.component.html') }, + { title: 'SCSS', language: 'scss', code: require('!!raw-loader!./template/datepicker-template.component.scss') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./template/datepicker-template.component.ts') }, + ]; + + monthYearSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./month-year-picker/month-year-picker.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./month-year-picker/month-year-picker.component.ts') }, + ]; + + rangeTypeSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./range-type/range-type-picker.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./range-type/range-type-picker.component.ts') }, + ]; + + rangeTemplateSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./range-template/range-template.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./range-template/range-template.component.ts') }, + ]; + + hostTemplateSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./host-template/datepicker-host-template.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./host-template/datepicker-host-template.component.ts') }, + ]; + + DatepickerProDemoStaticPanel: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./static-panel/datepicker-pro-static-panel.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./static-panel/datepicker-pro-static-panel.component.ts') }, + { title: 'SCSS', language: 'css', code: require('!!raw-loader!./static-panel/datepicker-pro-static-panel.component.scss') }, + ]; + + SelectDatepickerProDemo: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./select-type/select-type.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./select-type/select-type.component.ts') }, + { title: 'SCSS', language: 'css', code: require('!!raw-loader!./select-type/select-type.component.scss') }, + ]; + + navItems = []; + subs: Subscription = new Subscription(); + constructor(private translate: TranslateService) {} + + ngOnInit() { + this.subs.add( + this.translate.get('components.datepicker-pro.anchorLinkValues').subscribe((res) => { + this.setNavValues(res); + }) + ); + + this.subs.add( + this.translate.onLangChange.subscribe((event: TranslationChangeEvent) => { + const values = this.translate.instant('components.datepicker-pro.anchorLinkValues'); + this.setNavValues(values); + }) + ); + } + + setNavValues(values) { + this.navItems = [ + { dAnchorLink: 'basic-usage', value: values['basic-usage'] }, + { dAnchorLink: 'show-time', value: values['show-time'] }, + { dAnchorLink: 'template', value: values['template'] }, + { dAnchorLink: 'monthYear', value: values['monthYear'] }, + { dAnchorLink: 'range-picker', value: values['rangePicker'] }, + { dAnchorLink: 'range-template', value: values['rangeTemplate'] }, + { dAnchorLink: 'host-template', value: values['host-template'] }, + { dAnchorLink: 'datepicker-pro-static-panel', value: values['datepicker-pro-static-panel']}, + { dAnchorLink: 'select-type', value: values['select-type'] }, + ]; + } + + ngOnDestroy() { + if (this.subs) { + this.subs.unsubscribe(); + } + } +} diff --git a/devui/datepicker-pro/demo/datepicker-pro-demo.module.ts b/devui/datepicker-pro/demo/datepicker-pro-demo.module.ts new file mode 100644 index 00000000..9fefbca9 --- /dev/null +++ b/devui/datepicker-pro/demo/datepicker-pro-demo.module.ts @@ -0,0 +1,66 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { AutoCompleteModule } from 'ng-devui/auto-complete'; +import { ButtonModule } from 'ng-devui/button'; +import { DatepickerProModule } from 'ng-devui/datepicker-pro'; +import { SelectModule } from 'ng-devui/select'; +import { DevUIApiComponent } from 'ng-devui/shared/devui-api/devui-api.component'; +import { DevUIApiModule } from 'ng-devui/shared/devui-api/devui-api.module'; +import { DevUICodeboxModule } from 'ng-devui/shared/devui-codebox/devui-codebox.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { DDemoNavModule } from 'src/app/component/d-demo-nav.module'; +import { BasicDatepickerProComponent } from './basic/basic-datepicker-pro.component'; +import { DatepickerProDemoComponent } from './datepicker-pro-demo.component'; +import { DatepickerProHostComponent } from './host-template/datepicker-host-template.component'; +import { MonthYearDatepickerProComponent } from './month-year-picker/month-year-picker.component'; +import { RangeTemplatePickerComponent } from './range-template/range-template.component'; +import { RangeTypepickerProComponent } from './range-type/range-type-picker.component'; +import { SelectDatepickerDemoComponent } from './select-type/select-type.component'; +import { ShowTimeDatepickerProComponent } from './show-time/show-time-picker.component'; +import { DatepickerProStaticPanelComponent } from './static-panel/datepicker-pro-static-panel.component'; +import { DatepickerProTemplateComponent } from './template/datepicker-template.component'; + +@NgModule({ + imports: [ + TranslateModule, + CommonModule, + FormsModule, + AutoCompleteModule, + DevUIApiModule, + DevUICodeboxModule, + ButtonModule, + SelectModule, + DDemoNavModule, + DatepickerProModule, + RouterModule.forChild([ + { path: '', redirectTo: 'demo' }, + { path: 'demo', component: DatepickerProDemoComponent }, + { + path: 'api', + component: DevUIApiComponent, + data: { + 'zh-cn': require('!html-loader!markdown-loader!../doc/api-cn.md'), + 'en-us': require('!html-loader!markdown-loader!../doc/api-en.md'), + }, + }, + ]), + ], + exports: [DatepickerProDemoComponent], + declarations: [ + DatepickerProDemoComponent, + BasicDatepickerProComponent, + ShowTimeDatepickerProComponent, + DatepickerProTemplateComponent, + MonthYearDatepickerProComponent, + RangeTypepickerProComponent, + DatepickerProHostComponent, + RangeTemplatePickerComponent, + DatepickerProStaticPanelComponent, + SelectDatepickerDemoComponent + ], + providers: [], + +}) +export class DatepickerProDemoModule {} diff --git a/devui/datepicker-pro/demo/host-template/datepicker-host-template.component.html b/devui/datepicker-pro/demo/host-template/datepicker-host-template.component.html new file mode 100644 index 00000000..80ca0722 --- /dev/null +++ b/devui/datepicker-pro/demo/host-template/datepicker-host-template.component.html @@ -0,0 +1,26 @@ +
single datepicker
+ + + + + {{ value || 'select time' }} + + + + + clear + +
range datepicker
+ + + + + {{ values[0] || 'start' }} + + -- + + {{ values[1] || 'end' }} + + + + clear diff --git a/devui/datepicker-pro/demo/host-template/datepicker-host-template.component.ts b/devui/datepicker-pro/demo/host-template/datepicker-host-template.component.ts new file mode 100644 index 00000000..fe2f25b9 --- /dev/null +++ b/devui/datepicker-pro/demo/host-template/datepicker-host-template.component.ts @@ -0,0 +1,18 @@ +import { + Component, ViewChild +} from '@angular/core'; +import { DatepickerProComponent, RangeDatepickerProComponent } from 'ng-devui/datepicker-pro'; + +@Component({ +selector: 'd-datepicker-pro-host', +templateUrl: './datepicker-host-template.component.html', +}) +export class DatepickerProHostComponent { + @ViewChild(RangeDatepickerProComponent) rangePicker: RangeDatepickerProComponent; + @ViewChild(DatepickerProComponent) singlePicker: DatepickerProComponent; + value1 = new Date(); + value2 = []; + constructor() { + } + +} diff --git a/devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.html b/devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.html new file mode 100644 index 00000000..39c9d0bb --- /dev/null +++ b/devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.html @@ -0,0 +1,7 @@ +
month picker
+ + + +
year picker
+ + diff --git a/devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.ts b/devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.ts new file mode 100644 index 00000000..eb08707d --- /dev/null +++ b/devui/datepicker-pro/demo/month-year-picker/month-year-picker.component.ts @@ -0,0 +1,16 @@ +import { + Component +} from '@angular/core'; + +@Component({ +selector: 'd-month-year-datepicker-pro', +templateUrl: './month-year-picker.component.html', +}) +export class MonthYearDatepickerProComponent { + value1 = new Date(); + minDate = '2021/01/01'; + maxDate = '2031/12/30'; + constructor() { + } + +} diff --git a/devui/datepicker-pro/demo/range-template/range-template.component.html b/devui/datepicker-pro/demo/range-template/range-template.component.html new file mode 100644 index 00000000..ddb54c7d --- /dev/null +++ b/devui/datepicker-pro/demo/range-template/range-template.component.html @@ -0,0 +1,21 @@ + + +
    +
  • + before a month +
  • +
  • + before a week +
  • +
  • + this week +
  • +
+
+ + + +
diff --git a/devui/datepicker-pro/demo/range-template/range-template.component.scss b/devui/datepicker-pro/demo/range-template/range-template.component.scss new file mode 100644 index 00000000..5b62562d --- /dev/null +++ b/devui/datepicker-pro/demo/range-template/range-template.component.scss @@ -0,0 +1,12 @@ +.right-panel { + padding: 8px 16px; + text-align: left; +} + +.footer-panel { + text-align: right; + + d-button { + margin-right: 4px; + } +} diff --git a/devui/datepicker-pro/demo/range-template/range-template.component.ts b/devui/datepicker-pro/demo/range-template/range-template.component.ts new file mode 100644 index 00000000..40ac701a --- /dev/null +++ b/devui/datepicker-pro/demo/range-template/range-template.component.ts @@ -0,0 +1,44 @@ +import { + Component, ViewChild +} from '@angular/core'; +import { RangeDatepickerProComponent } from 'ng-devui/datepicker-pro'; + +@Component({ + selector: 'd-range-template-picker', + templateUrl: './range-template.component.html', + styleUrls: ['./range-template.component.scss'] +}) +export class RangeTemplatePickerComponent { + @ViewChild(RangeDatepickerProComponent) rangePicker: RangeDatepickerProComponent; + value1 = [new Date('2020/03/01'), new Date('2020/04/20')]; + today = new Date(); + constructor() { + } + + setDate(days: number) { + this.value1 = [ + new Date(this.today.getTime() + days * 24 * 3600 * 1000), + this.today + ]; + } + + selectThisWeek() { + const start = new Date(new Date().setDate(this.today.getDate() - this.today.getDay())); + const end = new Date(new Date().setDate(start.getDate() + 6)); + this.value1 = [ + start, + end + ]; + } + + clearStart() { + this.value1 = [null, this.value1[1]]; + this.rangePicker.focusChange('start'); + } + + clearEnd() { + this.value1 = [this.value1[0], null]; + this.rangePicker.focusChange('end'); + } + +} diff --git a/devui/datepicker-pro/demo/range-type/range-type-picker.component.html b/devui/datepicker-pro/demo/range-type/range-type-picker.component.html new file mode 100644 index 00000000..f0769c14 --- /dev/null +++ b/devui/datepicker-pro/demo/range-type/range-type-picker.component.html @@ -0,0 +1,19 @@ +
basic range picker
+ + + +
time range picker
+ + + +
week range picker
+ + + +
month range picker
+ + + +
year range picker
+ + diff --git a/devui/datepicker-pro/demo/range-type/range-type-picker.component.ts b/devui/datepicker-pro/demo/range-type/range-type-picker.component.ts new file mode 100644 index 00000000..e6079349 --- /dev/null +++ b/devui/datepicker-pro/demo/range-type/range-type-picker.component.ts @@ -0,0 +1,25 @@ +import { + Component +} from '@angular/core'; + +@Component({ +selector: 'd-range-type-picker', +templateUrl: './range-type-picker.component.html', +}) +export class RangeTypepickerProComponent { + value1 = [new Date('2020/03/01'), new Date('2020/04/20')]; + value2 = [new Date('2020/03/01'), new Date('2020/04/20')]; + value3 = []; + value4 = []; + value5 = []; + minDate = new Date(new Date().setMonth(new Date().getMonth() - 1)); + maxDate = new Date(new Date().setMonth(new Date().getMonth() + 2)); + constructor() { + + } + + onChange(dateList) { + console.log(dateList); + } + +} diff --git a/devui/datepicker-pro/demo/select-type/select-type.component.html b/devui/datepicker-pro/demo/select-type/select-type.component.html new file mode 100644 index 00000000..f5788063 --- /dev/null +++ b/devui/datepicker-pro/demo/select-type/select-type.component.html @@ -0,0 +1,26 @@ +
+ +
+ + + + + diff --git a/devui/datepicker-pro/demo/select-type/select-type.component.scss b/devui/datepicker-pro/demo/select-type/select-type.component.scss new file mode 100644 index 00000000..9768c71e --- /dev/null +++ b/devui/datepicker-pro/demo/select-type/select-type.component.scss @@ -0,0 +1,35 @@ +@import '~ng-devui/styles-var/devui-var.scss'; + +d-datepicker-calendar { + box-shadow: $devui-shadow-length-connected-overlay $devui-shadow; +} + +.right-panel { + height: 100%; + width: 80px; + padding: 8px 16px; + text-align: left; + position: relative; + + li { + position: absolute; + bottom: 50; + + d-button { + width: 50px; + } + + span { + margin-left: 8px; + color: #8a8e99; + } + } +} + +.footer-panel { + text-align: right; + + d-button { + margin-right: 4px; + } +} diff --git a/devui/datepicker-pro/demo/select-type/select-type.component.ts b/devui/datepicker-pro/demo/select-type/select-type.component.ts new file mode 100644 index 00000000..b44434ee --- /dev/null +++ b/devui/datepicker-pro/demo/select-type/select-type.component.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; + +const ONE_HOUR_TIME = 60 * 60 * 1000; +const ONE_DAY_TIME = ONE_HOUR_TIME * 24; +const ONE_WEEK_TIME = ONE_DAY_TIME * 7; +@Component({ + selector: 'd-demo-select-datepicker', + templateUrl: './select-type.component.html', + styleUrls: ['./select-type.component.scss'], +}) +export class SelectDatepickerDemoComponent { + value2 = [new Date('2021-4-5'), new Date('2021-4-8')]; + + activeType = 'start'; + + showPanel = false; + + options = [{ + id: 1, + name: '最近1h', + value: [new Date(new Date().getTime() - ONE_HOUR_TIME), new Date()] + }, { + id: 2, + name: '最近一天', + value: [new Date(new Date().getTime() - ONE_DAY_TIME), new Date()] + }, { + id: 3, + name: '最近一周', + value: [new Date(new Date().getTime() - ONE_WEEK_TIME), new Date()] + }, { + id: 4, + name: '自定义范围', + value: [] + }]; + currentOption = {}; + + onChange(value) { + if (value.id === 4) { + this.showPanel = true; + } + } + + ensureDate() { + this.showPanel = false; + this.options[3].name = this.options[3].value.map(d => d.toLocaleDateString()).join(' - '); + this.options = [...this.options]; + } + + switchType() { + this.activeType = this.activeType === 'start' ? 'end' : 'start'; + } +} diff --git a/devui/datepicker-pro/demo/show-time/show-time-picker.component.html b/devui/datepicker-pro/demo/show-time/show-time-picker.component.html new file mode 100644 index 00000000..c5cebc55 --- /dev/null +++ b/devui/datepicker-pro/demo/show-time/show-time-picker.component.html @@ -0,0 +1,2 @@ + + diff --git a/devui/datepicker-pro/demo/show-time/show-time-picker.component.ts b/devui/datepicker-pro/demo/show-time/show-time-picker.component.ts new file mode 100644 index 00000000..4b1cafe5 --- /dev/null +++ b/devui/datepicker-pro/demo/show-time/show-time-picker.component.ts @@ -0,0 +1,14 @@ +import { + Component +} from '@angular/core'; + +@Component({ +selector: 'd-show-time-datepicker-pro', +templateUrl: './show-time-picker.component.html', +}) +export class ShowTimeDatepickerProComponent { + value = new Date(); + minDate = new Date(new Date().setMonth(0)); + constructor() { + } +} diff --git a/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.html b/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.html new file mode 100644 index 00000000..8e93b4e8 --- /dev/null +++ b/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.html @@ -0,0 +1,5 @@ +

single calendar panel

+
+ + +
diff --git a/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.scss b/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.scss new file mode 100644 index 00000000..e0246718 --- /dev/null +++ b/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.scss @@ -0,0 +1,5 @@ +@import '~ng-devui/styles-var/devui-var.scss'; + +d-datepicker-calendar { + box-shadow: $devui-shadow-length-connected-overlay $devui-shadow; +} diff --git a/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.ts b/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.ts new file mode 100644 index 00000000..5bc806fc --- /dev/null +++ b/devui/datepicker-pro/demo/static-panel/datepicker-pro-static-panel.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +const ONE_HOUR_TIME = 60 * 60 * 1000; +const ONE_DAY_TIME = ONE_HOUR_TIME * 24; +const ONE_WEEK_TIME = ONE_DAY_TIME * 7; +@Component({ + selector: 'd-demo-datepicker-pro-static-panel', + templateUrl: './datepicker-pro-static-panel.component.html', + styleUrls: ['./datepicker-pro-static-panel.component.scss'], +}) +export class DatepickerProStaticPanelComponent { + + value1 = new Date(); + + onClickEnsure(date) { + console.log(date); + } +} diff --git a/devui/datepicker-pro/demo/template/datepicker-template.component.html b/devui/datepicker-pro/demo/template/datepicker-template.component.html new file mode 100644 index 00000000..d488a35a --- /dev/null +++ b/devui/datepicker-pro/demo/template/datepicker-template.component.html @@ -0,0 +1,35 @@ +
right side template
+ + + +
    +
  • + 一个月前 + {{ getDateString(-30) }} +
  • +
  • + 两周前 + {{ getDateString(-14) }} +
  • +
  • + 一周前 + {{ getDateString(-7) }} +
  • +
  • + 今天 + {{ getDateString(0) }} +
  • +
+
+
+ +
footer template
+ + + + + + diff --git a/devui/datepicker-pro/demo/template/datepicker-template.component.scss b/devui/datepicker-pro/demo/template/datepicker-template.component.scss new file mode 100644 index 00000000..36e1c168 --- /dev/null +++ b/devui/datepicker-pro/demo/template/datepicker-template.component.scss @@ -0,0 +1,23 @@ +.right-panel { + padding: 8px 16px; + text-align: left; + + li { + d-button { + width: 50px; + } + + span { + margin-left: 8px; + color: #8a8e99; + } + } +} + +.footer-panel { + text-align: right; + + d-button { + margin-right: 4px; + } +} diff --git a/devui/datepicker-pro/demo/template/datepicker-template.component.ts b/devui/datepicker-pro/demo/template/datepicker-template.component.ts new file mode 100644 index 00000000..a6f734a1 --- /dev/null +++ b/devui/datepicker-pro/demo/template/datepicker-template.component.ts @@ -0,0 +1,32 @@ +import { + Component, ViewChild +} from '@angular/core'; +import { DatepickerProComponent } from 'ng-devui/datepicker-pro'; + +@Component({ +selector: 'd-datepicker-pro-template', +styleUrls: ['./datepicker-template.component.scss'], +templateUrl: './datepicker-template.component.html', +}) +export class DatepickerProTemplateComponent { + @ViewChild('datepicker') datepickerPro: DatepickerProComponent; + value = new Date(); + today = new Date(); + + constructor() { + } + + setDate(days: number) { + this.value = new Date(this.today.getTime() + days * 24 * 3600 * 1000); + } + + clear() { + this.datepickerPro.clear(); + } + + getDateString(days: number): string { + const date = new Date(this.today.getTime() + days * 24 * 3600 * 1000); + return `${date.getMonth() + 1}月${date.getDate()}日`; + } + +} diff --git a/devui/datepicker-pro/doc/api-cn.md b/devui/datepicker-pro/doc/api-cn.md new file mode 100644 index 00000000..e7ab4b86 --- /dev/null +++ b/devui/datepicker-pro/doc/api-cn.md @@ -0,0 +1,97 @@ +# 如何使用 +在 module 中引入: + +```ts +import { DatepickerProModule } from 'ng-devui/datepicker-pro'; +``` + +在页面中使用: + +``` + +``` + +# d-datepicker-pro + +## d-datepicker-pro 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| cssClass | `string` | -- | 可选,自定义 class | | +| format | [ng 自定义日期格式](https://angular.cn/api/common/DatePipe#custom-format-options) | 'y/MM/dd' \| 'y/MM/dd HH:mm:ss' | 可选,传入格式化,根据是否 showTime 区别不同默认值 | [基本用法](demo#basic-usage) | +| showTime | `boolean` | false | 可选,是否显示时分秒 | [显示时间](demo#show-time) | +| disabled | `boolean` | false | 可选,选择器是否禁用 | [基本用法](demo#basic-usage) | +| autoOpen | `boolean` | false | 可选,初始化是否直接展开 | [显示时间](demo#show-time) | +| calenderRange | `number[]` | [1970, 2099] | 可选,传入日历的年份范围 | [基本用法](demo#basic-usage) | +| minDate | `Date` | new Date(calenderRange[0]) | 可选,限制最小可选日期 | [基本用法](demo#basic-usage) | +| maxDate | `Date` | new Date(calenderRange[1]) | 可选,限制最大可选日期 | [基本用法](demo#basic-usage) | +| showAnimation | `boolean` | true | 可选,是否开启动画 | | +| width | `string` | - | 可设置选择器的宽度 | | +| mode | `'year' \| 'month' \| 'date'` | 'date' | 面板模式 | [年月选择器](demo#monthYear)| + +## d-datepicker-pro 事件 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| dropdownToggle | `EventEmitter` | -- | 下拉面板开启关闭时触发,返回开启(true)或关闭(false) | | +| confirmEvent | `EventEmitter` | -- | 点击确定按钮后触发,返回当前的Date值 | | + +# d-range-datepicker-pro + +## d-range-datepicker-pro 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| cssClass | `string` | -- | 可选,自定义 class | | +| format | [ng 自定义日期格式](https://angular.cn/api/common/DatePipe#custom-format-options) | 'y/MM/dd' \| 'y/MM/dd HH:mm:ss' | 可选,传入格式化,根据是否 showTime 区别不同默认值 | [基本用法](demo#basic-usage) | +| showTime | `boolean` | false | 可选,是否显示时分秒 | [范围选择器](demo#range-picker) | +| disabled | `boolean` | false | 可选,选择器是否禁用 | [基本用法](demo#basic-usage) | +| autoOpen | `boolean` | false | 可选,初始化是否直接展开 | [显示时间](demo#show-time) | +| calenderRange | `number[]` | [1970, 2099] | 可选,传入日历的年份范围 | [基本用法](demo#basic-usage) | +| minDate | `Date` | new Date(calenderRange[0]) | 可选,限制最小可选日期 | [基本用法](demo#basic-usage) | +| maxDate | `Date` | new Date(calenderRange[1]) | 可选,限制最大可选日期 | [基本用法](demo#basic-usage) | +| splitter | `string` | `-` | 可设置范围日期之间的连接符 | | +| showAnimation | `boolean` | true | 可选,是否开启动画 | | +| width | `string` | - | 可设置范围选择器的宽度 | | +| mode | `'year' \| 'month' \| 'date' \| 'week'` | 'date' | 面板模式 | [范围选择器](demo#range-picker)| +| startIndexOfWeek | `number` | 0 | 周选择时候,每周的开始时间,0表示周日,6表示周六,与Date.getDay()相同 | [范围选择器](demo#range-picker)| + +## d-range-datepicker-pro 事件 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| dropdownToggle | `EventEmitter` | -- | 下拉面板开启关闭时触发,返回开启(true)或关闭(false) | | +| confirmEvent | `EventEmitter` | -- | 点击确定按钮后触发,返回当前的Date范围值 | | + +# d-datepicker-static-panel + +## d-datepicker-static-panel 参数 +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| isRangeType | `boolean` | false | 可选,是否展示范围模式 | | +| mode | `'year' \| 'month' \| 'date' \| 'week'` | 'date' | 可选,展示类型 | | +| showTime | `boolean` | false | 可选,date模式下是否可配置时间 | | +| startIndexOfWeek | `number` | 0 | 周选择时候,每周的开始时间,0表示周日,6表示周六,与Date.getDay()相同 | | +| activeRangeType | `'start' \| 'end'` | 'start' | 可选,范围模式下激活开始或者结束日期 | | + +## d-datepicker-static-panel 事件 + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| confirmEvent | `EventEmitter` | -- | 点击确定按钮后触发,返回当前的Date范围值 | | + + +# ngModel + +ngModel 在单日期模式下绑定为`Date`。 + +ngModel 在范围模式下绑定为`Date[]`。 + +ngModelChange 可监听其变化。 + +# 自定义模板 +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :------------------: | :----------------: | :---------------: | :----------------------: | :----------: | +| customTemplate | `TemplateRef` | -- | 可选,右侧区域自定义模板 | [传入模板](demo#template) | +| footerTemplate | `TemplateRef` | -- | 可选,footer自定义模板 | [传入模板](demo#template) | +| hostTemplate | `TemplateRef` | -- | 可选,datepicker宿主的自定义模板 | [传入模板](demo#host-template) | diff --git a/devui/datepicker-pro/doc/api-en.md b/devui/datepicker-pro/doc/api-en.md new file mode 100644 index 00000000..2c685c27 --- /dev/null +++ b/devui/datepicker-pro/doc/api-en.md @@ -0,0 +1,96 @@ +# How to use +Import into module: + +```ts +import { DatepickerProModule } from' ng-devui/datepicker-pro'; +``` + +In the page: + +``` + +``` + +# d-datepicker-pro + +## d-datepicker-pro Parameter + +| Parameter | Type | Default | Description | Jump to Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| cssClass | `string` | -- | Optional, custom class | | +| format | [ng Customized date format] (https://angular.cn/api/common/DatePipe#custom-format-options) | 'y/MM/dd'\| 'y/MM/dd HH:mm:ss' | Optional. The format is transferred, the default value varies depending on whether showTime is set. | [Basic usage](demo#basic-usage) | +| showTime | `boolean` | false | Optional, indicating whether to display hour, minute, and second | [display time](demo#show-time) | +| disabled | `boolean` | false | Optional, indicating whether the selector is disabled | [Basic usage](demo#basic-usage) | +| autoOpen | `boolean` | false | Optional, indicating whether to expand the initialization directly. | [Display time](demo#show-time) | +| calenderRange | `number[]` | [1970, 2099] |: specifies the year range of the calendar. This parameter is optional. | [Basic usage] (demo#basic-usage) | +| minDate | `Date` | new Date(calenderRange[0]) | (optional) Restricts the minimum available date. | [Basic usage](demo#basic-usage) | +| maxDate | `Date` | new Date(calenderRange[1]) | (optional) Restricts the maximum date that can be selected. | [Basic usage](demo#basic-usage) | +| showAnimation | `boolean` | true | (optional) Whether to enable animation | | +| width | `string` | - | Width of the selector. | | +| mode | `'year' \| 'month' \| 'date'` | 'date' | panel mode | [year and month selector](demo#monthYear)| + +## d-datepicker-pro event + +| Parameter | Type | Default | Description | Jump to Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +Triggered when the | dropdownToggle | `EventEmitter` | -- | drop-down panel is enabled or disabled. The options are true or false. | | +| confirmEvent | `EventEmitter` | -- | Triggered after the OK button is clicked. The current Date value is returned. | | + +# d-range-datepicker-pro + +## d-range-datepicker-pro Parameter + +| Parameter | Type | Default | Description | Jump to Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| cssClass | `string` | -- | Optional, custom class | | +| format | [ng Customized date format] (https://angular.cn/api/common/DatePipe#custom-format-options) | 'y/MM/dd'\| 'y/MM/dd HH:mm:ss' | Optional. The format is transferred, the default value varies depending on whether showTime is set. | [Basic usage](demo#basic-usage) | +| showTime | `boolean` | false | Optional, indicating whether to display hour, minute, and second | [range selector](demo#range-picker) | +| disabled | `boolean` | false | Optional, indicating whether the selector is disabled | [Basic usage](demo#basic-usage) | +| autoOpen | `boolean` | false | Optional, indicating whether to expand the initialization directly. | [Display time](demo#show-time) | +| calenderRange | `number[]` | [1970, 2099] |: specifies the year range of the calendar. This parameter is optional. | [Basic usage] (demo#basic-usage) | +| minDate | `Date` | new Date(calenderRange[0]) | (optional) Restricts the minimum available date. | [Basic usage](demo#basic-usage) | +| maxDate | `Date` | new Date(calenderRange[1]) | (optional) Restricts the maximum date that can be selected. | [Basic usage](demo#basic-usage) | +| splitter | `string` | `-` | The separator between dates can be set. | | +| showAnimation | `boolean` | true | (optional) Whether to enable animation | | +| width | `string` | - | Width of the range selector. | | +| mode | `year' \| 'month' \| 'date'\| 'week'`| 'date' | panel mode | [range selector](demo#range-picker)| +| startIndexOfWeek | `number` | 0 |: start time of a week. 0 indicates Sunday and 6 indicates Saturday, which is the same as Date.getDay().|[range selector](demo#range-picker)| + +## d-range-datepicker-pro event + +| Parameter | Type | Default | Description | Jump to Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| dropdownToggle | `EventEmitter` | -- | Triggered when the drop-down panel is enabled or disabled. The options are true or false. | | +| confirmEvent | `EventEmitter` | -- | Triggered after the OK button is clicked. The current Date range is returned. | | + +# d-datepicker-static-panel + +## d-datepicker-static-panel parameters +| Parameter | Type | Default | Description | Jump to Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| isRangeType | `boolean` | false |: indicates whether to display the scope mode. This parameter is optional. | | +| mode | `'year' \| 'month' \| 'date' \| 'week'` | 'date' | (Optional) Display type | | | +| showTime | `boolean` | false | Optional. Indicates whether the time can be configured in date mode. | | +| startIndexOfWeek | `number` | 0 |: start time of a week. The value 0 indicates Sunday and 6 indicates Saturday. The value is the same as that of Date.getDay(). | | +| activeRangeType | `start' \| 'end'`|'start' | Optional. Activation start or end date in range mode | | + +## d-datepicker-static-panel event + +| Parameter | Type | Default | Description | Jump to Demo | +| :-------: | :-------: | :-------: | :-------: | :-------: | +| confirmEvent | `EventEmitter` | -- | Triggered after the OK button is clicked. The current Date range is returned. | | + +# ngModel + +In single-date mode, ngModel is bound as `Date`. + +ngModel is bound as `Date[]` in range mode. + +ngModelChange can listen to the change. + +# Customize a template. +| Parameter | Type | Default Value | Description | Jump to Demo | +| :------------------: | :----------------: | :---------------: | :----------------------: | :----------: | +| customTemplate | `TemplateRef` | -- |: optional. Customize a template in the right pane. | [Input template](demo#template) | +| footerTemplate | `TemplateRef` | -- |: optional. Footer customized template | [Input template](demo#template) | +| hostTemplate | `TemplateRef` | -- | Optional. Custom template of the datepicker host | [Incoming template] (demo#host-template) | diff --git a/devui/datepicker-pro/index.ts b/devui/datepicker-pro/index.ts new file mode 100644 index 00000000..7e1a213e --- /dev/null +++ b/devui/datepicker-pro/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/devui/datepicker-pro/lib/calendar-panel.component.html b/devui/datepicker-pro/lib/calendar-panel.component.html new file mode 100644 index 00000000..f513dd9c --- /dev/null +++ b/devui/datepicker-pro/lib/calendar-panel.component.html @@ -0,0 +1,73 @@ +
+ +
+

{{ item.year }}

+

{{ item.year }}

+

+ {{ i18nText.monthsOfYear[item.month] }} +

+
+
+ +
+ + + + + + + + + + + +
{{ item }}
+ +
+

{{ i18nText?.getYearMonthStr(month.year, month.month + 1) }}

+ + + + + + +
+ {{ day.inMonth ? day.day : '' }} +
+
+
+
+
+
diff --git a/devui/datepicker-pro/lib/calendar-panel.component.scss b/devui/datepicker-pro/lib/calendar-panel.component.scss new file mode 100644 index 00000000..581c7497 --- /dev/null +++ b/devui/datepicker-pro/lib/calendar-panel.component.scss @@ -0,0 +1,253 @@ +@import '../../style/theme/color'; +@import '../../style/theme/shadow'; +@import '../../style/theme/corner'; + +.devui-canlender-panel { + text-align: center; + // border-right: 1px solid $devui-dividing-line; + font-size: 0; + + &-collopse-button { + position: absolute; + top: 0; + left: 60px; + font-size: 16px; + z-index: 10; + cursor: pointer; + color: $devui-aide-text; + } + + &-year-list { + padding: 8px 0; + display: inline-block; + width: 80px; + height: 305px; + overflow: auto; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0 !important; + } + + &-item { + background-color: $devui-global-bg; + + .devui-year-title { + font-size: 14px; + font-weight: bold; + line-height: 30px; + cursor: pointer; + } + + .devui-month-title { + font-size: 12px; + line-height: 30px; + cursor: pointer; + } + + &.title-active { + background-color: $devui-base-bg; + } + + &:hover:not(.title-active) { + background-color: $devui-list-item-selected-bg; + } + } + } + + &-main { + width: 249px; + height: 305px; + padding: 4px; + display: inline-block; + font-size: 12px; + } +} + +.devui-tbody-wrapper { + height: 270px; + width: 100%; + overflow-x: hidden; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0 !important; + } +} + +.devui-table { + border-spacing: 0; + + .devui-week-header td { + width: 34px; + height: 22px; + } + + .devui-table-month-title { + text-align: start; + line-height: 26px; + color: $devui-aide-text; + padding-left: 8px; + } + + .devui-month-table { + color: $devui-text; + border-spacing: 0 4px; + border-collapse: separate; + + .devui-table-date { + padding: 0 4px; + width: 30px; + + span { + cursor: pointer; + display: block; + width: 22px; + height: 22px; + line-height: 22px; + border-radius: $devui-border-radius-feedback; + } + + &:not(.devui-table-date-disable):not(.devui-table-date-selected):hover { + span { + background-color: $devui-list-item-hover-bg; + } + } + + &.devui-table-date-today { + span { + color: $devui-brand; + } + } + + &.devui-table-date-selected { + position: relative; + + span { + background: $devui-list-item-active-bg; + color: $devui-list-item-active-text; + + &:hover { + box-shadow: none; + border-color: transparent; + } + } + + &.devui-table-date-abandon-selected { + span { + background: $devui-primary-disabled; + } + } + + &.devui-table-date-active-type:not(.devui-table-date-abandon-selected) span { + animation: 2s ease 0s infinite normal both breath-animation; + position: absolute; + top: 0; + right: 7px; + z-index: 2; + } + + &.devui-table-date-active-type.devui-table-date-end:not(.devui-table-date-abandon-selected) span { + right: 8px; + } + @keyframes breath-animation { + 0% { + box-shadow: 0 0 0 $devui-list-item-active-bg; + } + + 50% { + box-shadow: 0 0 8px $devui-list-item-active-bg; + } + + to { + box-shadow: 0 0 0 $devui-list-item-active-bg; + } + } + } + + &.devui-table-date-inrange { + background-color: $devui-list-item-hover-bg; + + span:hover { + background-color: $devui-range-item-hover-bg; + } + } + + &.devui-table-date-start:not(.devui-table-date-end) { + position: relative; + + &::after { + content: ''; + display: block; + position: absolute; + width: 8px; + height: 22px; + background-color: $devui-list-item-hover-bg; + right: 0; + top: 0; + } + + &:not(:hover):not(.devui-table-date-selected)::after { + display: none; + } + + span { + border-radius: $devui-border-radius-feedback 0 0 $devui-border-radius-feedback; + } + } + + &.devui-table-date-end:not(.devui-table-date-start) { + position: relative; + + &::after { + content: ''; + display: block; + position: absolute; + width: 5px; + height: 22px; + background-color: $devui-list-item-hover-bg; + left: 0; + top: 0; + } + + span { + border-radius: 0 $devui-border-radius-feedback $devui-border-radius-feedback 0; + } + } + + &.devui-table-date-in-selected-range:not(.devui-table-date-inrange):not(:hover) { + background-color: $devui-disabled-bg; + } + + &.devui-table-date-disable span { + cursor: not-allowed; + color: $devui-disabled-text; + + &:hover { + box-shadow: none; + border-color: transparent; + } + } + } + + &.devui-single-date .devui-table-date { + &.devui-table-date-start::after { + display: none; + } + + &.devui-table-date-end::after { + display: none; + } + } + } + + .devui-tbody-wrapper { + transform: none; + } +} + +:host { + display: inline-block; +} diff --git a/devui/datepicker-pro/lib/calendar-panel.component.ts b/devui/datepicker-pro/lib/calendar-panel.component.ts new file mode 100644 index 00000000..8dae7f7e --- /dev/null +++ b/devui/datepicker-pro/lib/calendar-panel.component.ts @@ -0,0 +1,452 @@ +import { CdkScrollable, CdkVirtualScrollViewport, ScrollDispatcher } from '@angular/cdk/scrolling'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, Input, OnDestroy, OnInit, ViewChild +} from '@angular/core'; +import { I18nInterface, I18nService } from 'ng-devui/i18n'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, filter, takeUntil } from 'rxjs/operators'; +import { DatepickerProCommonDataService, DatepickerProService } from '../datepicker-pro.service'; +import { DevuiCalendarDateItem } from './datepicker-pro.type'; + +const DAY_DURATION = 24 * 60 * 60 * 1000; + +@Component({ + selector: 'd-calendar-panel', + templateUrl: './calendar-panel.component.html', + styleUrls: ['./calendar-panel.component.scss'], + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CalendarPanelComponent implements OnInit, OnDestroy { + @ViewChild('scrollBody') scrollBodyCmp: CdkVirtualScrollViewport; + @ViewChild('scrollList') scrollListCmp: CdkVirtualScrollViewport; + @Input() isRangeType: boolean; + @Input() isWeekSelect = false; + + currentBodyIndex = 0; + today: Date; + calendarItemSize = 186; // 一个月份日历的高度 + + i18nText: I18nInterface['datePickerPro']; + i18nSubscription: Subscription; + unsubscribe$ = new Subject(); + + allMonthList = []; + yearAndMonthList = []; + scrollListener: Subscription; + isListCollopse = false; + weekHoverRange = []; + curWeekHoverDate: Date; + + get curHoverDate() { + return this.pickerSrv.curHoverDate; + } + + set curHoverDate(value: Date) { + this.pickerSrv.curHoverDate = value; + } + + get curDate(): Date { + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + return this.pickerSrv.curRangeDate[0]; + } else if (this.pickerSrv.currentActiveInput === 'end') { + return this.pickerSrv.curRangeDate[1]; + } + } else { + return this.pickerSrv.curDate; + } + } + + set curDate(value: Date) { + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.curRangeDate[0] = value; + } else if (this.pickerSrv.currentActiveInput === 'end') { + if (this.pickerSrv.showTime) { + this.pickerSrv.curRangeDate[1] = this.pickerSrv.curRangeDate[1] ? value : new Date(value.setHours(23, 59, 59)); + } else { + this.pickerSrv.curRangeDate[1] = new Date(value.setHours(23, 59, 59)); + } + } + } else { + this.pickerSrv.curDate = value; + } + } + + get selectedRangeDate(): Date[] { + return this.pickerSrv.curRangeDate; + } + + set selectedRangeDate(dateList: Date[]) { + this.pickerSrv.curRangeDate = dateList; + } + + constructor( + protected i18n: I18nService, + private pickerSrv: DatepickerProService, + private dataSrv: DatepickerProCommonDataService, + private cdr: ChangeDetectorRef, + private scrollDispatcher: ScrollDispatcher + ) { + } + + setI18nText() { + this.i18nText = this.i18n.getI18nText().datePickerPro; + this.i18nSubscription = this.i18n.langChange().pipe( + takeUntil(this.unsubscribe$) + ).subscribe((data) => { + this.i18nText = data.datePickerPro; + this.cdr.detectChanges(); + }); + } + + ngOnInit(): void { + this.setI18nText(); + this.today = new Date(); + this.initDataList(); + this.initObservable(); + } + + private initObservable() { + this.pickerSrv.toggleEvent.pipe( + takeUntil(this.unsubscribe$), + ).subscribe(isOpen => { + if (isOpen) { + setTimeout(() => { + this.goToDate(this.curDate || new Date(), 'auto'); + }); + + // 首次展开添加滚动监听 + if (!this.scrollListener) { + this.initScrollListener(); + } + } + }); + + this.pickerSrv.updateDateValue.pipe( + takeUntil(this.unsubscribe$), + ).subscribe(res => { + if (this.isRangeType) { + this.updateRangeDate(res.value as Date[]); + } else { + this.updateSingleDate(res.value as Date); + } + }); + + this.pickerSrv.activeInputChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(type => { + if (type === 'start') { + this.goToDate(this.selectedRangeDate[0] || this.selectedRangeDate[1] || new Date()); + } else { + this.goToDate(this.selectedRangeDate[1] || this.selectedRangeDate[0] || new Date()); + } + }); + } + + private initScrollListener() { + this.scrollDispatcher.scrolled().pipe( + takeUntil(this.unsubscribe$), + filter((res: CdkScrollable) => { + return res && res.getElementRef().nativeElement.classList.contains('devui-tbody-wrapper'); + }), + debounceTime(50) + ).subscribe(() => { + const offsetY = this.scrollBodyCmp.measureScrollOffset(); + // 当滚动超过一个月面板的一半时,就更新月份 + this.currentBodyIndex = Math.floor(offsetY / this.calendarItemSize) + (offsetY % this.calendarItemSize > 100 ? 1 : 0); + const listIndex = this.isListCollopse ? + Math.floor(this.currentBodyIndex / 12) : this.currentBodyIndex + Math.floor(this.currentBodyIndex / 12) + 1; + this.goToListByIndex(listIndex); + this.cdr.detectChanges(); + }); + } + + private initDataList() { + const key = `${this.pickerSrv.calendarRange.join('-')}-${ + this.pickerSrv.minDate.toDateString() + this.pickerSrv.maxDate.toDateString() + }`; + if (this.dataSrv.calendarDataCache[key]) { + this.yearAndMonthList = this.dataSrv.calendarDataCache[key].yearAndMonthList; + this.allMonthList = this.dataSrv.calendarDataCache[key].allMonthList; + return; + } + + this.yearAndMonthList = []; + this.allMonthList = []; + + for (let yearIndex = this.pickerSrv.calendarRange[0]; yearIndex <= this.pickerSrv.calendarRange[1]; yearIndex++) { + this.yearAndMonthList.push({ + year: yearIndex, + isMonth: false, + active: false + }); + for (let monthIndex = 0; monthIndex < 12; monthIndex++) { + this.allMonthList.push({ + year: yearIndex, + month: monthIndex, + displayWeeks: this.getDisplayWeeks(yearIndex, monthIndex) + }); + + this.yearAndMonthList.push({ + year: yearIndex, + month: monthIndex, + isMonth: true, + active: false + }); + } + } + + this.dataSrv.calendarDataCache[key] = { + yearAndMonthList: this.yearAndMonthList, + allMonthList: this.allMonthList + }; + } + + private getDisplayWeeks(yearindex: number, monthIndex: number): DevuiCalendarDateItem[] { + const firstDayOfMonth = new Date(yearindex, monthIndex, 1); + const weekOfDay = firstDayOfMonth.getDay(); + const startDate = new Date(firstDayOfMonth.getTime() - weekOfDay * DAY_DURATION); + const displayWeeks = []; + for (let i = 0; i < 6; i++) { + const startWeekDate = startDate.getTime() + i * 7 * DAY_DURATION; + const weekDays = new Array(7).fill(0).map((value, index) => { + const currentDate = new Date(startWeekDate + index * DAY_DURATION); + return { + day: this.fillLeft(currentDate.getDate()), + date: currentDate, + inMonth: currentDate.getMonth().toString() === monthIndex.toString(), + isToday: currentDate.toDateString() === this.today.toDateString(), + isDisable: !this.pickerSrv.dateInRange(currentDate) + }; + }); + displayWeeks.push(weekDays); + } + return displayWeeks; + } + + private goToDate(date: Date, scrollBehavior?) { + const indexObj = this.getCurrentIndex(date); + const scroll = scrollBehavior || (Math.abs(indexObj.bodyIndex - this.currentBodyIndex) > 18 ? 'auto' : 'smooth'); + this.currentBodyIndex = indexObj.bodyIndex; + this.scrollBodyCmp.scrollToIndex(indexObj.bodyIndex, scroll); + this.goToListByIndex(indexObj.listIndex); + this.cdr.detectChanges(); + } + + private goToListByIndex(index) { + const indexDelta = Math.abs(this.scrollListCmp.measureScrollOffset() / 30 - index); + this.scrollListCmp.scrollToIndex(index - 4, indexDelta < 12 ? 'smooth' : 'auto'); + this.updateListActive(index); + } + + selectMonth(year: number, month: number) { + const date = new Date(year, month, 1); + const curYear = this.yearAndMonthList.find(t => t.active)?.year || this.curDate.getFullYear(); + // 太远的虚拟滚动会导致白屏,所以超过两年的滚动都直接跳转 + const isSmoothAnimation = Math.abs(curYear - year) < 2; + + if (this.isListCollopse) { + this.toggleListCollopse(date); + } else { + this.goToDate(date, isSmoothAnimation ? 'smooth' : 'auto'); + } + } + + updateRangeDate(dateList: Date[]) { + if (!dateList) { + this.selectedRangeDate = []; + this.cdr.detectChanges(); + return; + } + + const curDate = (this.pickerSrv.currentActiveInput === 'start' ? + (dateList[0] || dateList[1]) : (dateList[1] || dateList[0])) || new Date(); + const morethanOneYear = Math.abs(curDate.getFullYear() - (this.currentBodyIndex / 12 + this.pickerSrv.calendarRange[0])) > 1; + this.selectedRangeDate = dateList; + this.goToDate(curDate, morethanOneYear ? 'auto' : 'smooth'); + this.cdr.detectChanges(); + } + + updateSingleDate(date: Date) { + if (!date) { + this.curDate = null; + this.cdr.detectChanges(); + return; + } + const morethanOneYear = Math.abs(date.getFullYear() - (this.currentBodyIndex / 12 + this.pickerSrv.calendarRange[0])) > 1; + this.curDate = date; + this.goToDate(this.curDate, morethanOneYear ? 'auto' : 'smooth'); + this.cdr.detectChanges(); + } + + updateListActive(index: number) { + const curActive = this.yearAndMonthList.find(t => t.active); + if (curActive) { + curActive.active = false; + } + this.yearAndMonthList[index].active = true; + } + + getCurrentIndex(curDate: Date) { + const year = curDate.getFullYear(); + const month = curDate.getMonth(); + const listIndex = this.isListCollopse ? + (year - this.pickerSrv.calendarRange[0]) : (year - this.pickerSrv.calendarRange[0]) * 13 + month + 1; + return { + bodyIndex: (year - this.pickerSrv.calendarRange[0]) * 12 + month, + listIndex + }; + } + + selectDate(day: DevuiCalendarDateItem) { + if (day.isDisable || !day.inMonth) { + return; + } + + if (this.isWeekSelect) { + this.pickerSrv.curRangeDate = this.getWeekRange(day.date); + this.pickerSrv.currentActiveInput = 'end'; + } else { + this.curDate = new Date(day.date.setHours(this.pickerSrv.curHour, this.pickerSrv.curMin, this.pickerSrv.curSec)); + } + if (this.isRangeType) { + this.pickerSrv.fixRangeDate(); + } + this.cdr.detectChanges(); + // 非时间模式下选完开始日期跳转到结束日期 + if (this.isRangeType && !this.pickerSrv.showTime) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.currentActiveInput = 'end'; + } else if (this.pickerSrv.currentActiveInput === 'end' && !this.selectedRangeDate[0]) { + this.pickerSrv.currentActiveInput = 'start'; + } else { + this.pickerSrv.closeDropdownEvent.next(); + } + } + this.pickerSrv.selectedDateChange.next({ + type: this.isRangeType ? 'range' : 'single', + value: this.isRangeType ? this.selectedRangeDate : this.curDate + }); + + if (this.isRangeType && this.pickerSrv.showTime) { + this.pickerSrv.updateTimeChange.next({ + activeInput: this.pickerSrv.currentActiveInput, + hour: this.pickerSrv.curHour, + min: this.pickerSrv.curMin, + seconds: this.pickerSrv.curSec + }); + } + + if (this.pickerSrv.closeAfterSelected) { + this.pickerSrv.closeDropdownEvent.next(); + } + } + + isStartDate(date: Date): boolean { + return this.pickerSrv.isStartDate(date); + } + + isEndDate(date: Date): boolean { + return this.pickerSrv.isEndDate(date); + } + + isDateInRange(date: Date): boolean { + if (this.isWeekSelect) { + return this.isInWeekHoverRange(date); + } else { + return this.pickerSrv.isDateInRange(date); + } + } + + isDateInSelectRange(date: Date): boolean { + return this.pickerSrv.isDateInSelectRange(date); + } + + isDateActive(date: Date): boolean { + return this.pickerSrv.isDateActive(date); + } + + isActiveTypeDate(date: Date): boolean { + return this.pickerSrv.isActiveInputTypeDate(date); + } + + isDateAbandon(date: Date): boolean { + return this.pickerSrv.isDateAbandon(date); + } + + isSingleDate(): boolean { + if (this.pickerSrv.currentActiveInput === 'start') { + return !this.pickerSrv.curRangeDate[1]; + } else if (this.pickerSrv.currentActiveInput === 'end') { + return !this.pickerSrv.curRangeDate[0]; + } + } + + setHoverTarget(date: Date, isInMonth: boolean) { + if (!isInMonth) { + this.curHoverDate = null; + return; + } + if (this.isWeekSelect) { + this.weekHoverRange = this.getWeekRange(date); + this.curWeekHoverDate = date; + return; + } + + if (this.isRangeType) { + this.curHoverDate = date; + } + } + + getWeekRange(date: Date) { + if (!date) { + return []; + } + const weekStart = new Date(date.getTime() - (date.getDay() - this.pickerSrv.startIndexOfWeek) * DAY_DURATION); + const weekEnd = new Date(weekStart.getTime() + DAY_DURATION * 6); + weekEnd.setHours(23, 59, 59); + + return [weekStart, weekEnd]; + } + + isInWeekHoverRange(date: Date) { + const range = this.getWeekRange(this.curWeekHoverDate); + const time = date.getTime(); + const timeStr = date.toDateString(); + if (this.pickerSrv.isDateActive(date)) { + return false; + } + return (this.pickerSrv.isDateInRange(date) || + (range[0]?.getTime() < time && time < range[1]?.getTime()) || + (range[0]?.toDateString() === timeStr || timeStr === range[1]?.toDateString())); + } + + toggleListCollopse(toDate?: Date) { + const activeItem = this.yearAndMonthList.find(t => t.active); + const curYear = activeItem?.year; + const curMonth = activeItem?.month; + this.isListCollopse = !this.isListCollopse; + if (this.isListCollopse) { + this.yearAndMonthList = this.yearAndMonthList.filter(t => !t.isMonth); + } else { + this.initDataList(); + } + setTimeout(() => { + this.goToDate(toDate || new Date(curYear, curMonth || 0, 1), 'auto'); + }); + } + + protected fillLeft(num: number) { + return num < 10 ? `0${num}` : `${num}`; + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + +} diff --git a/devui/datepicker-pro/lib/datepicker-pro.type.ts b/devui/datepicker-pro/lib/datepicker-pro.type.ts new file mode 100644 index 00000000..fb2a7025 --- /dev/null +++ b/devui/datepicker-pro/lib/datepicker-pro.type.ts @@ -0,0 +1,21 @@ +export interface DevuiCalendarDateItem { + day: string; + date: Date; + isActive: boolean; + inMonth: boolean; + isToday: boolean; + isInRange?: boolean; + isDisable?: boolean; +} + +export interface DateConfig { + dateConverter: any; + min: Date; // 默认1900 + max: Date; // 默认 2099 + format: { + date: string; // 默认 'y/MM/dd' + time: string; // 默认 'y/MM/dd HH:mm', + month: string; + year: string; + }; +} diff --git a/devui/datepicker-pro/lib/footer-panel.component.html b/devui/datepicker-pro/lib/footer-panel.component.html new file mode 100644 index 00000000..59732cc7 --- /dev/null +++ b/devui/datepicker-pro/lib/footer-panel.component.html @@ -0,0 +1,8 @@ + + + + {{ i18nText.btnOk }} + {{ i18nText.btnCancel }} + diff --git a/devui/datepicker-pro/lib/footer-panel.component.scss b/devui/datepicker-pro/lib/footer-panel.component.scss new file mode 100644 index 00000000..528507df --- /dev/null +++ b/devui/datepicker-pro/lib/footer-panel.component.scss @@ -0,0 +1,12 @@ +@import '../../style/theme/color'; + +.devui-datepicekr-pro-footer { + padding: 8px; + text-align: center; + // height: 44px; + border-top: 1px solid $devui-dividing-line; + + .devui-cancel-button { + margin-left: 4px; + } +} diff --git a/devui/datepicker-pro/lib/footer-panel.component.ts b/devui/datepicker-pro/lib/footer-panel.component.ts new file mode 100644 index 00000000..0abe933b --- /dev/null +++ b/devui/datepicker-pro/lib/footer-panel.component.ts @@ -0,0 +1,71 @@ +import { + Component, Input, OnDestroy, TemplateRef +} from '@angular/core'; +import { I18nInterface, I18nService } from 'ng-devui/i18n'; +import { Subscription } from 'rxjs'; +import { DatepickerProService } from '../datepicker-pro.service'; + +@Component({ + selector: 'd-datepicker-footer-panel', + templateUrl: './footer-panel.component.html', + styleUrls: ['./footer-panel.component.scss'], + preserveWhitespaces: false, +}) +export class FooterPanelComponent implements OnDestroy { + @Input() footerTemplate: TemplateRef; + + i18nText: I18nInterface['common']; + i18nSubscription: Subscription; + + get isRange() { + return this.pickerSrv.isRange; + } + + get confirmDisable() { + if (this.isRange) { + return (this.pickerSrv.currentActiveInput === 'start' && !this.pickerSrv.curRangeDate[0]) || + (this.pickerSrv.currentActiveInput === 'end' && !this.pickerSrv.curRangeDate[1]); + } else { + return false; + } + } + + constructor( + private pickerSrv: DatepickerProService, + protected i18n: I18nService + ) { + this.setI18nText(); + } + + setI18nText() { + this.i18nText = this.i18n.getI18nText().common; + this.i18nSubscription = this.i18n.langChange().subscribe((data) => { + this.i18nText = data.common; + }); + } + + ensureDate() { + if (this.pickerSrv.isRange) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.currentActiveInput = 'end'; + this.pickerSrv.activeInputChange.next('end'); + } else if (!this.pickerSrv.curRangeDate[0]) { + this.pickerSrv.currentActiveInput = 'start'; + this.pickerSrv.activeInputChange.next('start'); + } else { + this.close(true); + } + } else { + this.close(true); + } + } + + close(isConfirm = false) { + this.pickerSrv.closeDropdownEvent.next(isConfirm); + } + + ngOnDestroy() { + this.i18nSubscription.unsubscribe(); + } + +} diff --git a/devui/datepicker-pro/lib/month-panel.component.html b/devui/datepicker-pro/lib/month-panel.component.html new file mode 100644 index 00000000..5720e634 --- /dev/null +++ b/devui/datepicker-pro/lib/month-panel.component.html @@ -0,0 +1,51 @@ +
+ +
+

{{ year.year }}

+
+
+
+ +
+

{{ year.year + '年' }}

+ + + + + + +
+ + {{ i18nText.monthsOfYear[month - 1] }} + +
+
+
+
+
diff --git a/devui/datepicker-pro/lib/month-panel.component.scss b/devui/datepicker-pro/lib/month-panel.component.scss new file mode 100644 index 00000000..02d801b4 --- /dev/null +++ b/devui/datepicker-pro/lib/month-panel.component.scss @@ -0,0 +1,143 @@ +@import '../../style/theme/color'; +@import '../../style/theme/corner'; + +.devui-month-panel { + height: 300px; + font-size: 0; + + .devui-year-list { + padding: 8px 0; + display: inline-block; + width: 80px; + height: 300px; + overflow: auto; + text-align: center; + font-size: 12px; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0 !important; + } + + &-item { + height: 30px; + line-height: 30px; + background-color: $devui-global-bg; + cursor: pointer; + + &.title-active { + background-color: $devui-base-bg; + } + + &:hover:not(.title-active) { + background-color: $devui-list-item-selected-bg; + } + } + } + + .devui-month-list-wrapper { + padding: 0 4px; + display: inline-block; + } + + .devui-month-list { + width: 180px; + height: 295px; + font-size: 12px; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0 !important; + } + + .devui-table-year-title { + text-align: start; + line-height: 26px; + color: $devui-aide-text; + padding-left: 8px; + } + + .devui-month-table { + border-collapse: collapse; + } + + .devui-month-item { + width: 60px; + height: 40px; + padding: 4px 0; + box-sizing: border-box; + text-align: center; + cursor: pointer; + + &:hover span:not(.devui-disabled) { + background-color: $devui-list-item-hover-bg; + } + + span { + display: block; + line-height: 32px; + border-radius: $devui-border-radius-feedback; + + &.devui-disabled { + border-radius: 0; + } + } + + &.devui-table-this-month span { + color: $devui-brand; + } + + &.devui-table-month-selected { + span { + background: $devui-list-item-active-bg; + color: $devui-list-item-active-text; + + &:hover { + background-color: $devui-list-item-active-bg; + } + } + + &.devui-table-date-abandon-selected { + span { + background: $devui-primary-disabled; + } + } + } + + &.devui-table-date-inrange:not(.devui-table-month-selected) span { + background-color: $devui-list-item-hover-bg; + border-radius: 0; + } + + &.devui-table-date-start:not(.devui-table-date-end) span { + border-radius: $devui-border-radius-feedback 0 0 $devui-border-radius-feedback; + } + + &.devui-table-date-end:not(.devui-table-date-start) span { + border-radius: 0 $devui-border-radius-feedback $devui-border-radius-feedback 0; + } + + &.devui-table-date-in-selected-range:not(.devui-table-date-inrange):not(:hover) span { + background-color: $devui-disabled-bg; + border-radius: 0; + } + + &.devui-table-date-active-type:not(.devui-table-date-abandon-selected) span { + animation: 2s ease 0s infinite normal both breath-animation; + } + @keyframes breath-animation { + 0% { + box-shadow: 0 0 0 $devui-list-item-active-bg; + } + + 50% { + box-shadow: 0 0 8px $devui-list-item-active-bg; + } + + to { + box-shadow: 0 0 0 $devui-list-item-active-bg; + } + } + } + } +} diff --git a/devui/datepicker-pro/lib/month-panel.component.ts b/devui/datepicker-pro/lib/month-panel.component.ts new file mode 100644 index 00000000..abea51a5 --- /dev/null +++ b/devui/datepicker-pro/lib/month-panel.component.ts @@ -0,0 +1,288 @@ +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { I18nInterface, I18nService } from 'ng-devui/i18n'; +import { fromEvent, Subject, Subscription } from 'rxjs'; +import { debounceTime, takeUntil } from 'rxjs/operators'; +import { DatepickerProService } from './../datepicker-pro.service'; + +@Component({ + selector: 'd-month-panel', + templateUrl: './month-panel.component.html', + styleUrls: ['./month-panel.component.scss'], + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MonthPanelComponent implements OnInit, OnDestroy { + @ViewChild('scrollBody') scrollBodyCmp: CdkVirtualScrollViewport; + @ViewChild('scrollList') scrollListCmp: CdkVirtualScrollViewport; + @Input() isRangeType = false; + + unsubscribe$ = new Subject(); + scrollListener: Subscription; + i18nText: I18nInterface['datePickerPro']; + i18nSubscription: Subscription; + + yearList = []; + calenderItemSize = 186; + currentBodyIndex; + monthList = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12] + ]; + thisMonth: Date; + + get curHoverDate() { + return this.pickerSrv.curHoverDate; + } + + set curHoverDate(value: Date) { + this.pickerSrv.curHoverDate = value; + } + + get selectedRangeDate(): Date[] { + return this.pickerSrv.curRangeDate; + } + + set selectedRangeDate(dateList: Date[]) { + this.pickerSrv.curRangeDate = dateList; + } + + get currentDate(): Date { + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + return this.pickerSrv.curRangeDate[0]; + } else if (this.pickerSrv.currentActiveInput === 'end') { + return this.pickerSrv.curRangeDate[1]; + } + } else { + return this.pickerSrv.curDate; + } + } + + set currentDate(value: Date) { + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.curRangeDate[0] = value; + } else if (this.pickerSrv.currentActiveInput === 'end') { + this.pickerSrv.curRangeDate[1] = value; + } + } else { + this.pickerSrv.curDate = value; + } + } + + constructor( + protected i18n: I18nService, + private pickerSrv: DatepickerProService, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit() { + this.thisMonth = new Date(); + this.initList(); + this.initObservable(); + this.setI18nText(); + } + + setI18nText() { + this.i18nText = this.i18n.getI18nText().datePickerPro; + this.i18nSubscription = this.i18n.langChange().subscribe((data) => { + this.i18nText = data.datePickerPro; + this.cdr.detectChanges(); + }); + } + + initObservable() { + this.pickerSrv.toggleEvent.pipe( + takeUntil(this.unsubscribe$), + ).subscribe(isOpen => { + if (isOpen) { + setTimeout(() => { + this.goToDate(this.currentDate || new Date()); + }); + } + if (!this.scrollListener) { + this.initScrollListener(); + } + }); + + this.pickerSrv.updateDateValue.pipe( + takeUntil(this.unsubscribe$), + ).subscribe(res => { + if (res.type === 'range') { + this.pickerSrv.curRangeDate = res.value as Date[]; + } else { + this.pickerSrv.curDate = res.value as Date; + } + this.goToDate(this.currentDate || new Date()); + this.cdr.detectChanges(); + }); + + this.pickerSrv.activeInputChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(type => { + if (type === 'start') { + this.goToDate(this.selectedRangeDate[0] || this.selectedRangeDate[1] || new Date()); + } else { + this.goToDate(this.selectedRangeDate[1] || this.selectedRangeDate[0] || new Date()); + } + }); + } + + initList() { + for (let yearIndex = this.pickerSrv.calendarRange[0]; yearIndex <= this.pickerSrv.calendarRange[1]; yearIndex++) { + this.yearList.push({ + year: yearIndex, + active: false + }); + } + } + + initScrollListener() { + const scrollEle = this.scrollBodyCmp.getElementRef().nativeElement; + this.scrollListener = fromEvent(scrollEle, 'mousewheel').pipe( + takeUntil(this.unsubscribe$), + debounceTime(180) // 滚动动画时间180,延时保证计算位置的准确 + ).subscribe(() => { + const offsetY = this.scrollBodyCmp.measureScrollOffset(); + // 当面板滚过一半更新面板 + this.currentBodyIndex = Math.floor(offsetY / this.calenderItemSize) + (offsetY % this.calenderItemSize > 100 ? 1 : 0); + this.goToListByIndex(this.currentBodyIndex); + this.cdr.detectChanges(); + }); + } + + goToDate(date: Date, scrollBehavior: 'auto' | 'smooth' = 'auto') { + if (date) { + const index = date.getFullYear() - this.pickerSrv.calendarRange[0]; + this.currentBodyIndex = index; + this.scrollBodyCmp.scrollToIndex(index, scrollBehavior); + this.goToListByIndex(index); + } + } + + goToListByIndex(index) { + const indexDelta = Math.abs(this.scrollListCmp.measureScrollOffset() / 30 - index); + this.scrollListCmp.scrollToIndex(index - 4, indexDelta < 12 ? 'smooth' : 'auto'); + } + + selectMonth(yearIndex: number, monthIndex: number) { + if (this.isDisable(yearIndex, monthIndex)) { + return; + } + this.currentDate = new Date(yearIndex, monthIndex - 1, 1); + + if (this.isRangeType) { + this.pickerSrv.fixRangeDate(); + } + + // 非时间模式下选完开始日期跳转到结束日期 + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.currentActiveInput = 'end'; + } else if (this.pickerSrv.currentActiveInput === 'end' && !this.selectedRangeDate[0]) { + this.pickerSrv.currentActiveInput = 'start'; + } else { + this.pickerSrv.closeDropdownEvent.next(); + } + } + + this.pickerSrv.selectedDateChange.next({ + type: this.isRangeType ? 'range' : 'single', + value: this.isRangeType ? this.selectedRangeDate : this.currentDate + }); + + if (this.pickerSrv.closeAfterSelected) { + this.pickerSrv.closeDropdownEvent.next(); + } + } + + selectYear(yearIndex: number) { + const isScroll = (yearIndex - this.pickerSrv.calendarRange[0]) - this.currentBodyIndex < 7; + this.goToDate(new Date(yearIndex, 0, 1), isScroll ? 'smooth' : 'auto'); + } + + isSelected(yearIndex: number, monthIndex: number) { + return this.pickerSrv.isMonthActive(yearIndex, monthIndex - 1); + } + + isThisMonth(yearIndex: number, monthIndex: number) { + return this.thisMonth.getFullYear() === yearIndex && this.thisMonth.getMonth() === monthIndex - 1; + } + + isDateInRange(yearIndex: number, monthIndex: number) { + const date = new Date(yearIndex, monthIndex - 1, 1); + return this.pickerSrv.isDateInRange(date); + } + + isDisable(yearIndex: number, monthIndex: number) { + const date = new Date(yearIndex, monthIndex - 1, 1); + return this.pickerSrv.maxDate.getTime() < date.getTime() || this.pickerSrv.minDate.getTime() > date.getTime(); + } + + isStartDate(yearIndex: number, monthIndex: number): boolean { + if (!this.isRangeType) { + return false; + } + const date = new Date(yearIndex, monthIndex - 1, 1); + return this.pickerSrv.isStartDate(date); + } + + isEndDate(yearIndex: number, monthIndex: number): boolean { + if (!this.isRangeType) { + return false; + } + const date = new Date(yearIndex, monthIndex - 1, 1); + return this.pickerSrv.isEndDate(date); + } + + isDateAbandon(yearIndex: number, monthIndex: number) { + if (!this.isRangeType || (!this.selectedRangeDate[0] || !this.selectedRangeDate[1])) { + return false; + } + const date = new Date(yearIndex, monthIndex - 1, 1); + return this.pickerSrv.isDateAbandon(date); + } + + isDateInSelectRange(yearIndex: number, monthIndex: number) { + if (!this.isRangeType) { + return false; + } + + const date = new Date(yearIndex, monthIndex - 1, 1); + + return this.pickerSrv.isDateInSelectRange(date); + } + + isActiveTypeDate(yearIndex: number, monthIndex: number) { + if (!this.isRangeType) { + return false; + } + + const date = new Date(yearIndex, monthIndex - 1, 1); + + return this.pickerSrv.isActiveInputTypeDate(date); + } + + setHoverTarget(yearIndex: number, monthIndex: number) { + const date = new Date(yearIndex, monthIndex - 1, 1); + if (this.isRangeType && !this.isDisable(yearIndex, monthIndex)) { + this.curHoverDate = date; + } + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } +} diff --git a/devui/datepicker-pro/lib/timepicker-panel.component.html b/devui/datepicker-pro/lib/timepicker-panel.component.html new file mode 100644 index 00000000..cd4b0bda --- /dev/null +++ b/devui/datepicker-pro/lib/timepicker-panel.component.html @@ -0,0 +1,48 @@ +
+
+ {{ i18nText?.hour }} + {{ i18nText?.min }} + {{ i18nText?.second }} +
+
+
    +
  • + {{ item?.time }} +
  • +
+
    +
  • + {{ item?.time }} +
  • +
+
    +
  • + {{ item?.time }} +
  • +
+
+
diff --git a/devui/datepicker-pro/lib/timepicker-panel.component.scss b/devui/datepicker-pro/lib/timepicker-panel.component.scss new file mode 100644 index 00000000..69f0c773 --- /dev/null +++ b/devui/datepicker-pro/lib/timepicker-panel.component.scss @@ -0,0 +1,95 @@ +@import '../../style/theme/color'; +@import '../../style/theme/shadow'; +@import '../../style/theme/corner'; + +:host { + display: inline-block; + vertical-align: top; + font-size: 12px; +} + +.devui-timepicker-panel { + width: 114px; + height: 306px; + text-align: center; + border-left: 1px solid $devui-dividing-line; + + .devui-time-header { + display: flex; + height: 30px; + padding: 0 6px; + + &-item { + flex: 1; + line-height: 30px; + margin: 0 6px; + } + } + + .devui-time-picker { + display: flex; + height: 276px; + width: 114px; + padding: 0 6px; + overflow: hidden; + border-radius: $devui-border-radius $devui-border-radius 0 0; + + .devui-time-list { + flex: 1; + overflow-x: hidden; + overflow-y: scroll; + overflow-y: overlay; + scroll-behavior: auto; + padding: 0 6px; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0 !important; + } + + &:hover { + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px !important; + } + } + + .devui-time-item { + width: 100%; + height: 22px; + line-height: 22px; + text-align: center; + cursor: pointer; + + &:not(:last-child) { + margin-bottom: 8px; + } + + &:last-child { + margin-bottom: 260px; + } + } + } + + &:not(.devui-disabled) .devui-time-list .devui-time-item { + &:hover { + color: $devui-list-item-hover-text; + background-color: $devui-list-item-hover-bg; + border-radius: $devui-border-radius-feedback; + } + + &.active { + color: $devui-list-item-active-text; + background-color: $devui-list-item-active-bg; + border-radius: $devui-border-radius-feedback; + } + + &.disabled { + color: $devui-disabled-text; + background-color: $devui-disabled-bg; + border-radius: $devui-border-radius-feedback; + } + } + } +} diff --git a/devui/datepicker-pro/lib/timepicker-panel.component.ts b/devui/datepicker-pro/lib/timepicker-panel.component.ts new file mode 100644 index 00000000..6c5e684b --- /dev/null +++ b/devui/datepicker-pro/lib/timepicker-panel.component.ts @@ -0,0 +1,207 @@ +import { + Component, ElementRef, OnDestroy, OnInit +} from '@angular/core'; +import { I18nInterface, I18nService } from 'ng-devui/i18n'; +import { Subject, Subscription } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DatepickerProService } from '../datepicker-pro.service'; + +interface TimeObj { + time: string; + type?: string; + active?: boolean; + disabled?: boolean; +} + +@Component({ + selector: 'd-timepicker-panel', + templateUrl: './timepicker-panel.component.html', + styleUrls: ['./timepicker-panel.component.scss'], + preserveWhitespaces: false, +}) +export class TimepickerPanelComponent implements OnInit, OnDestroy { + + firstList: Array = []; + secondList: Array = []; + thirdList: Array = []; + + hourIndex = null; + minIndex = null; + secIndex = null; + + typeList = ['hour', 'min', 'sec']; + + unsubscribe$ = new Subject(); + i18nText: I18nInterface['datePickerPro']; + i18nSubscription: Subscription; + + constructor( + private el: ElementRef, + private pickSrv: DatepickerProService, + protected i18n: I18nService + ) {} + + ngOnInit(): void { + this.initDateList(); + this.initObservable(); + this.setI18nText(); + } + + setI18nText() { + this.i18nText = this.i18n.getI18nText().datePickerPro; + this.i18nSubscription = this.i18n.langChange().subscribe((data) => { + this.i18nText = data.datePickerPro; + }); + } + + initObservable() { + this.pickSrv.toggleEvent.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(isOpen => { + if (isOpen) { + setTimeout(() => { + this.initDateList(true); + }); + } + }); + + this.pickSrv.updateTimeChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(value => { + if (this.hourIndex !== value.hour) { + this.chooseTime('hour', value.hour); + } + + if (this.minIndex !== value.min) { + this.chooseTime('min', value.min); + } + + if (this.secIndex !== value.seconds) { + this.chooseTime('sec', value.seconds); + } + }); + + if (this.pickSrv.isRange) { + this.pickSrv.activeInputChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(type => { + const isNull = (type === 'start' && !this.pickSrv.curRangeDate[0]) || + (type === 'end' && !this.pickSrv.curRangeDate[1]); + if (this.hourIndex !== this.pickSrv.curHour || isNull) { + this.chooseTime('hour', isNull ? null : this.pickSrv.curHour); + } + + if (this.minIndex !== this.pickSrv.curMin || isNull) { + this.chooseTime('min', isNull ? null : this.pickSrv.curMin); + } + + if (this.secIndex !== this.pickSrv.curSec || isNull) { + this.chooseTime('sec', isNull ? null : this.pickSrv.curSec); + } + }); + } + } + + initDateList(justScroll = false) { + if (this.pickSrv.curDate) { + this.hourIndex = this.pickSrv.curDate.getHours(); + this.minIndex = this.pickSrv.curDate.getMinutes(); + this.secIndex = this.pickSrv.curDate.getSeconds(); + } + this.typeList.forEach(type => { + this.chooseTime(type, this[`${type}Index`], false, justScroll); + }); + } + + chooseTime(type: string, index: number, handle = false, justScroll = false) { + switch (type) { + case 'hour': + this.hourIndex = index; + this.firstList = new Array(24).fill(1).map((t, i) => { + return { + time: i < 10 ? `0${i}` : i + '', + type: 'hour', + active: this.hourIndex === i, + disabled: false + }; + }); + this.setAllScroll(index, this.pickSrv.curMin, this.pickSrv.curSec, justScroll); + break; + case 'min': + this.minIndex = index; + this.secondList = new Array(60).fill(1).map((t, i) => { + return { + time: i < 10 ? `0${i}` : i + '', + type: 'min', + active: this.minIndex === i, + disabled: false + }; + }); + this.setAllScroll(this.pickSrv.curHour, index, this.pickSrv.curSec, justScroll); + break; + case 'sec': + this.secIndex = index; + this.thirdList = new Array(60).fill(1).map((t, i) => { + return { + time: i < 10 ? `0${i}` : i + '', + type: 'sec', + active: this.secIndex === i, + disabled: false + }; + }); + this.setAllScroll(this.pickSrv.curHour, this.pickSrv.curMin, index, justScroll); + } + + if (handle) { + this.pickSrv.selectedTimeChange.next({ + activeInput: this.pickSrv.currentActiveInput, + hour: this.hourIndex, + min: this.minIndex, + seconds: this.secIndex + }); + } + } + + setAllScroll(first: number, second: number, third: number, justScroll: boolean) { + this.setScroll('first', first, justScroll); + this.setScroll('second', second, justScroll); + this.setScroll('third', third, justScroll); + } + + setScroll(whichList, index, justScroll?) { + const scroll = (22 + 8) * index; + const duration = justScroll ? 0 : 100; + if (this.el) { + this.scrollTo(this.el.nativeElement.querySelector(`.devui-${whichList}-list`), scroll, duration); + } + } + + scrollTo(element: HTMLElement, to: number, duration: number): void { + if (typeof window === undefined || !element) { + return; + } + if (duration <= 0) { + element.scrollTop = to; + return; + } + const difference = to - element.scrollTop; + const perTick = (difference / duration) * 10; + const reqAnimFrame = window['requestAnimationFrame'] || + window['mozRequestAnimationFrame'] || + window['msRequestAnimationFrame'] || + window['oRequestAnimationFrame']; + reqAnimFrame(() => { + element.scrollTop = element.scrollTop + perTick; + if (element.scrollTop === to) { + return; + } + this.scrollTo(element, to, duration - 10); + }); + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + +} diff --git a/devui/datepicker-pro/lib/year-panel.component.html b/devui/datepicker-pro/lib/year-panel.component.html new file mode 100644 index 00000000..a0805211 --- /dev/null +++ b/devui/datepicker-pro/lib/year-panel.component.html @@ -0,0 +1,25 @@ +
+ +
+

+ {{ year }} +

+
+
+
diff --git a/devui/datepicker-pro/lib/year-panel.component.scss b/devui/datepicker-pro/lib/year-panel.component.scss new file mode 100644 index 00000000..2a65a4f5 --- /dev/null +++ b/devui/datepicker-pro/lib/year-panel.component.scss @@ -0,0 +1,109 @@ +@import '../../style/theme/color'; +@import '../../style/theme/corner'; + +.year-list-panel { + width: 204px; + height: 200px; + padding: 8px 12px; + + .devui-year-list { + width: 184px; + height: 186px; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0; + } + + &:hover { + overflow-y: overlay; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px; + } + } + + .devui-year-list-item { + padding: 4px 0; + height: 48px; + } + + .devui-year-title { + width: 60px; + height: 40px; + float: left; + font-size: 12px; + display: block; + text-align: center; + line-height: 40px; + cursor: pointer; + border-radius: $devui-border-radius-feedback; + + &:hover:not(.devui-active-year):not(.devui-disabled) { + background-color: $devui-list-item-hover-bg; + } + + &.devui-this-year { + color: $devui-brand; + } + + &.devui-active-year { + background: $devui-list-item-active-bg; + color: $devui-list-item-active-text; + + &.devui-table-date-abandon-selected { + background: $devui-primary-disabled; + } + } + + &.devui-table-date-inrange:not(.devui-active-year) { + background-color: $devui-list-item-hover-bg; + border-radius: 0; + } + + &.devui-table-date-start:not(.devui-table-date-end) { + border-radius: $devui-border-radius-feedback 0 0 $devui-border-radius-feedback; + } + + &.devui-table-date-end:not(.devui-table-date-start) { + border-radius: 0 $devui-border-radius-feedback $devui-border-radius-feedback 0; + } + + &.devui-table-date-in-selected-range:not(.devui-table-date-inrange):not(:hover) { + background-color: $devui-disabled-bg; + border-radius: 0; + } + + &.devui-table-date-active-type:not(.devui-table-date-abandon-selected) { + animation: 2s ease 0s infinite normal both breath-animation; + } + @keyframes breath-animation { + 0% { + box-shadow: 0 0 0 $devui-list-item-active-bg; + } + + 50% { + box-shadow: 0 0 8px $devui-list-item-active-bg; + } + + to { + box-shadow: 0 0 0 $devui-list-item-active-bg; + } + } + + &.devui-disabled { + border-radius: 0; + cursor: not-allowed; + + &:first-of-type { + border-radius: 4px 0 0 4px; + } + + &:last-of-type { + border-radius: 0 4px 4px 0; + } + } + } + } +} diff --git a/devui/datepicker-pro/lib/year-panel.component.ts b/devui/datepicker-pro/lib/year-panel.component.ts new file mode 100644 index 00000000..90699749 --- /dev/null +++ b/devui/datepicker-pro/lib/year-panel.component.ts @@ -0,0 +1,226 @@ +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { + ChangeDetectionStrategy, + + ChangeDetectorRef, + + Component, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { chunk } from 'lodash-es'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DatepickerProService } from '../datepicker-pro.service'; + +@Component({ + selector: 'd-year-panel', + templateUrl: './year-panel.component.html', + styleUrls: ['./year-panel.component.scss'], + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class YearPanelComponent implements OnInit, OnDestroy { + @ViewChild('scrollList') scrollListCmp: CdkVirtualScrollViewport; + @Input() isRangeType = false; + + yearList = []; + + unsubscribe$ = new Subject(); + + get curHoverDate() { + return this.pickerSrv.curHoverDate; + } + + set curHoverDate(value: Date) { + this.pickerSrv.curHoverDate = value; + } + + get currentDate() { + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + return this.pickerSrv.curRangeDate[0]; + } else if (this.pickerSrv.currentActiveInput === 'end') { + return this.pickerSrv.curRangeDate[1]; + } + } else { + return this.pickerSrv.curDate; + } + } + + set currentDate(value: Date) { + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.curRangeDate[0] = value; + } else if (this.pickerSrv.currentActiveInput === 'end') { + this.pickerSrv.curRangeDate[1] = value; + } + } else { + this.pickerSrv.curDate = value; + } + } + + constructor( + private cdr: ChangeDetectorRef, + private pickerSrv: DatepickerProService + ) { + } + + ngOnInit() { + this.initYearList(); + this.initObservable(); + } + + initObservable() { + this.pickerSrv.toggleEvent.subscribe(isOpen => { + if (isOpen) { + setTimeout(() => { + this.goToYear(this.currentDate || new Date()); + }); + } + }); + + this.pickerSrv.updateDateValue.pipe( + takeUntil(this.unsubscribe$), + ).subscribe(res => { + if (res.type === 'range') { + this.pickerSrv.curRangeDate = res.value as Date[]; + } else { + this.pickerSrv.curDate = res.value as Date; + } + this.goToYear(this.currentDate || new Date()); + }); + + this.pickerSrv.activeInputChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(type => { + if (type === 'start') { + this.goToYear(this.pickerSrv.curRangeDate[0] || this.pickerSrv.curRangeDate[1] || new Date()); + } else { + this.goToYear(this.pickerSrv.curRangeDate[1] || this.pickerSrv.curRangeDate[0] || new Date()); + } + }); + } + + initYearList() { + const list = new Array(this.pickerSrv.calendarRange[1] - this.pickerSrv.calendarRange[0] + 1).fill(1).map((t, i) => { + return i + this.pickerSrv.calendarRange[0]; + }); + this.yearList = chunk(list, 3); + } + + goToYear(date: Date) { + if (date) { + const index = Math.floor((date.getFullYear() - this.pickerSrv.calendarRange[0]) / 3); + this.scrollListCmp.scrollToIndex(index - 1); + } + this.cdr.detectChanges(); + } + + isThisYear(yearIndex: number): boolean { + return yearIndex === new Date().getFullYear(); + } + + isActiveYear(yearIndex: number): boolean { + return this.pickerSrv.isYearActive(yearIndex); + } + + isStartDate(yearIndex: number): boolean { + if (!this.isRangeType) { + return false; + } + const date = new Date(yearIndex, 0, 1); + return this.pickerSrv.isStartDate(date); + } + + isEndDate(yearIndex: number): boolean { + if (!this.isRangeType) { + return false; + } + const date = new Date(yearIndex, 0, 1); + return this.pickerSrv.isEndDate(date); + } + + isDateAbandon(yearIndex: number) { + if (!this.isRangeType || (!this.pickerSrv.curRangeDate[0] || !this.pickerSrv.curRangeDate[1])) { + return false; + } + const date = new Date(yearIndex, 0, 1); + return this.pickerSrv.isDateAbandon(date); + } + + isActiveTypeDate(yearIndex: number) { + if (!this.isRangeType) { + return false; + } + + const date = new Date(yearIndex, 0, 1); + + return this.pickerSrv.isActiveInputTypeDate(date); + } + + isDateInRange(yearIndex: number) { + const date = new Date(yearIndex, 0, 1); + return this.pickerSrv.isDateInRange(date); + } + + isDateInSelectRange(yearIndex: number) { + if (!this.isRangeType) { + return false; + } + + const date = new Date(yearIndex, 0, 1); + + return this.pickerSrv.isDateInSelectRange(date); + } + + selectYear(yearIndex: number) { + if (this.isDisable(yearIndex)) { + return; + } + this.currentDate = new Date(yearIndex, 0 , 1); + + if (this.isRangeType) { + this.pickerSrv.fixRangeDate(); + } + + // 非时间模式下选完开始日期跳转到结束日期 + if (this.isRangeType) { + if (this.pickerSrv.currentActiveInput === 'start') { + this.pickerSrv.currentActiveInput = 'end'; + } else if (this.pickerSrv.currentActiveInput === 'end' && !this.pickerSrv.curRangeDate[0]) { + this.pickerSrv.currentActiveInput = 'start'; + } else { + this.pickerSrv.closeDropdownEvent.next(); + } + } + + this.pickerSrv.selectedDateChange.next({ + type: this.isRangeType ? 'range' : 'single', + value: this.isRangeType ? this.pickerSrv.curRangeDate : this.currentDate + }); + + if (this.pickerSrv.closeAfterSelected) { + this.pickerSrv.closeDropdownEvent.next(); + } + } + + isDisable(yearIndex: number) { + const date = new Date(yearIndex, 0, 1); + return this.pickerSrv.maxDate.getTime() < date.getTime() || this.pickerSrv.minDate.getTime() > date.getTime(); + } + + setHoverTarget(yearIndex: number) { + const date = new Date(yearIndex, 0, 1); + if (this.isRangeType && !this.isDisable(yearIndex)) { + this.curHoverDate = date; + } + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } +} diff --git a/devui/datepicker-pro/package.json b/devui/datepicker-pro/package.json new file mode 100644 index 00000000..3b9bd979 --- /dev/null +++ b/devui/datepicker-pro/package.json @@ -0,0 +1,8 @@ +{ + "ngPackage": { + "lib": { + "entryFile": "public-api.ts" + } + } + } + \ No newline at end of file diff --git a/devui/datepicker-pro/public-api.ts b/devui/datepicker-pro/public-api.ts new file mode 100644 index 00000000..96eb50c6 --- /dev/null +++ b/devui/datepicker-pro/public-api.ts @@ -0,0 +1,12 @@ +export * from './datepicker-panel.component'; +export * from './datepicker-pro-calendar.component'; +export * from './datepicker-pro.component'; +export * from './datepicker-pro.module'; +export * from './datepicker-pro.service'; +export * from './lib/calendar-panel.component'; +export * from './lib/datepicker-pro.type'; +export * from './lib/footer-panel.component'; +export * from './lib/month-panel.component'; +export * from './lib/timepicker-panel.component'; +export * from './lib/year-panel.component'; +export * from './range-datepicker-pro.component'; diff --git a/devui/datepicker-pro/range-datepicker-pro.component.html b/devui/datepicker-pro/range-datepicker-pro.component.html new file mode 100644 index 00000000..a9d96b16 --- /dev/null +++ b/devui/datepicker-pro/range-datepicker-pro.component.html @@ -0,0 +1,80 @@ +
+
+ +
+ +
+ + + + + + + + + {{ splitter }} + +
+
+ +
+
+
+ +
+ + +
+
diff --git a/devui/datepicker-pro/range-datepicker-pro.component.scss b/devui/datepicker-pro/range-datepicker-pro.component.scss new file mode 100644 index 00000000..d03fa493 --- /dev/null +++ b/devui/datepicker-pro/range-datepicker-pro.component.scss @@ -0,0 +1,70 @@ +@import '../style/theme/color'; + +.devui-datepicker-pro-wrapper { + display: inline-block; + background-color: $devui-base-bg; + + .devui-range-picker { + padding: 0 8px; + position: relative; + box-sizing: border-box; + width: 100%; + min-height: 24px; + + .devui-input { + height: 24px; + padding: 4px 10px; + width: calc(50% - 18px); + + &.devui-active-input { + color: $devui-brand; + } + } + + .devui-date-input-active-bar { + display: block; + max-width: calc(50% - 48px); + height: 1px; + background-color: $devui-brand-foil; + position: absolute; + bottom: 0; + + &.devui-start-side { + left: 35px; + } + + &.devui-end-side { + left: calc(50% + 20px); + } + } + + &-icon { + vertical-align: bottom; + } + + .close-icon-wrapper { + padding: 0 8px; + visibility: hidden; + position: absolute; + right: 0; + top: 0; + line-height: 25px; + } + + &.devui-has-value:hover .close-icon-wrapper { + visibility: visible; + } + + &:not(.devui-disabled) .close-icon-wrapper { + cursor: pointer; + } + } + + .devui-input-group > .devui-input { + display: inline-block; + + &::-ms-clear { + display: none; + } + } +} diff --git a/devui/datepicker-pro/range-datepicker-pro.component.ts b/devui/datepicker-pro/range-datepicker-pro.component.ts new file mode 100644 index 00000000..4e60020a --- /dev/null +++ b/devui/datepicker-pro/range-datepicker-pro.component.ts @@ -0,0 +1,412 @@ +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { I18nInterface, I18nService } from 'ng-devui/i18n'; +import { DefaultDateConverter } from 'ng-devui/utils'; +import { fromEvent, Subject } from 'rxjs'; +import { debounceTime, map, takeUntil } from 'rxjs/operators'; +import { DatepickerProService } from './datepicker-pro.service'; +import { DateConfig } from './lib/datepicker-pro.type'; + +@Component({ + selector: 'd-range-datepicker-pro', + templateUrl: './range-datepicker-pro.component.html', + styleUrls: ['./range-datepicker-pro.component.scss'], + providers: [ + DatepickerProService, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RangeDatepickerProComponent), + multi: true + } + ], + preserveWhitespaces: false, +}) +export class RangeDatepickerProComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor { + @Input() mode: 'year' | 'month' | 'date' | 'week' = 'date'; + @Input() showTime = false; + @Input() disabled = false; + @Input() autoOpen = false; + @Input() format: string; + @Input() locale: string; + @Input() cssClass: string; + @Input() splitter = '-'; + @Input() width: string; + @Input() startIndexOfWeek = 0; + @Input() set calenderRange (value) { + this.pickerSrv.calendarRange = value || [1970, 2099]; + } + @Input() set minDate(value: Date) { + this.pickerSrv.minDate = value; + } + @Input() set maxDate(value: Date) { + this.pickerSrv.maxDate = value; + } + @Output() dropdownToggle = new EventEmitter(); + @Output() confirmEvent = new EventEmitter(); + @ContentChild('customTemplate') customTemplate: TemplateRef; + @ContentChild('footerTemplate') footerTemplate: TemplateRef; + @ContentChild('hostTemplate') hostTemplate: TemplateRef; + @ViewChild('dateInputStart') datepickerInputStart: ElementRef; + @ViewChild('dateInputEnd') datepickerInputEnd: ElementRef; + + private i18nLocale: I18nInterface['locale']; + + i18nText; + _dateValue = []; + datepickerConvert: DefaultDateConverter; + unsubscribe$ = new Subject(); + isOpen = false; + strWidth = 0; + + get dateValue() { + return this._dateValue; + } + + set dateValue(value: string[]) { + this._dateValue = value; + this.getStrWidth(); + } + + set currentActiveInput(value: 'start' | 'end') { + this.pickerSrv.currentActiveInput = value; + } + get currentActiveInput(): 'start' | 'end' { + return this.pickerSrv.currentActiveInput; + } + get dateConfig(): DateConfig { + return { + dateConverter: this.datepickerConvert, + min: this.pickerSrv.minDate || new Date(this.pickerSrv.calendarRange[0] + '/01/01'), + max: this.pickerSrv.maxDate || new Date(this.pickerSrv.calendarRange[1] + '/12/31'), + format: { + date: this.format || 'y/MM/dd', + time: this.format || 'y/MM/dd HH:mm:ss', + month: this.format || 'y-MM', + year: 'y' + } + }; + } + + get curFormat(): string { + if (this.mode === 'year') { + return this.dateConfig.format.year; + } else if (this.mode === 'month') { + return this.dateConfig.format.month; + } else { + return this.showTime ? this.dateConfig.format.time : this.dateConfig.format.date; + } + } + + get curActiveDate(): Date { + if (this.pickerSrv.currentActiveInput === 'start') { + return this.pickerSrv.curRangeDate[0] || this.pickerSrv.curRangeDate[1] || new Date(); + } else { + return this.pickerSrv.curRangeDate[1] || this.pickerSrv.curRangeDate[0] || new Date(); + } + } + + private onChange = (_: any) => null; + private onTouched = () => null; + + constructor( + private i18n: I18nService, + private pickerSrv: DatepickerProService, + private cdr: ChangeDetectorRef + ) { + this.i18nText = this.i18n.getI18nText().datePickerPro; + this.datepickerConvert = new DefaultDateConverter(); + } + + ngOnInit() { + this.initSrvStatus(); + this.setI18nText(); + setTimeout(() => { + this.isOpen = this.autoOpen; + }); + } + + ngAfterViewInit(): void { + this.initObservable(); + } + + private initSrvStatus() { + this.pickerSrv.showTime = this.showTime; + this.pickerSrv.isRange = true; + this.pickerSrv.startIndexOfWeek = this.startIndexOfWeek; + } + + private initObservable() { + this.pickerSrv.selectedDateChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(change => { + this.pickerSrv.curRangeDate = change.value as Date[]; + this.dateValue = (change.value as Date[]).map(d => this.formatDateToString(d)); + this.onChange(change.value); + }); + + this.pickerSrv.closeDropdownEvent.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(isConfirm => { + this.isOpen = false; + this.dropdownToggle.emit(false); + if (isConfirm) { + this.confirmEvent.emit(this.pickerSrv.curRangeDate); + } + }); + + this.pickerSrv.activeInputChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(() => { + this.getStrWidth(); + }); + + if (!this.hostTemplate) { + fromEvent(this.datepickerInputStart.nativeElement, 'input').pipe( + takeUntil(this.unsubscribe$), + map(t => { + this.getStrWidth(); + return 'start'; + }), + debounceTime(300) + ).subscribe(this.inputChangeCallback); + + fromEvent(this.datepickerInputEnd.nativeElement, 'input').pipe( + takeUntil(this.unsubscribe$), + map(t => { + this.getStrWidth(); + return 'end'; + }), + debounceTime(300) + ).subscribe(this.inputChangeCallback); + + fromEvent(this.datepickerInputStart.nativeElement, 'blur').pipe( + takeUntil(this.unsubscribe$), + map(t => 'start'), + ).subscribe(this.inputBlurCallback); + + fromEvent(this.datepickerInputEnd.nativeElement, 'blur').pipe( + takeUntil(this.unsubscribe$), + map(t => 'end'), + ).subscribe(this.inputBlurCallback); + } + + if (this.showTime) { + this.pickerSrv.selectedTimeChange.pipe( + takeUntil(this.unsubscribe$) + ).subscribe(change => { + const curTime = new Date(this.curActiveDate.getTime()).setHours(change.hour, change.min, change.seconds); + const curDate = new Date(curTime); + if (change.activeInput === 'start') { + this.pickerSrv.curRangeDate[0] = curDate; + if (this.isSameDateAndTimeWrong()) { + this.pickerSrv.curRangeDate[1] = curDate; + this.dateValue[1] = this.formatDateToString(curDate); + } + this.dateValue = [this.formatDateToString(curDate), this.dateValue[1]]; + } else { + this.pickerSrv.curRangeDate[1] = curDate; + if (this.isSameDateAndTimeWrong()) { + this.pickerSrv.curRangeDate[0] = curDate; + this.dateValue[0] = this.formatDateToString(curDate); + } + this.dateValue = [this.dateValue[0], this.formatDateToString(curDate)]; + } + this.onChange(this.pickerSrv.curRangeDate); + }); + } + } + + isSameDateAndTimeWrong(): boolean { + if (this.pickerSrv.curRangeDate[0]?.toDateString() === this.pickerSrv.curRangeDate[1]?.toDateString()) { + return this.pickerSrv.curRangeDate[0].getTime() > this.pickerSrv.curRangeDate[1].getTime(); + } + return false; + } + + getStrWidth() { + let str = this.pickerSrv.currentActiveInput === 'start' ? this.dateValue[0] : this.dateValue[1]; + if (!str || !str.length) { + str = this.pickerSrv.currentActiveInput === 'start' ? this.i18nText.startPlaceholder : this.i18nText.endPlaceholder; + } + this.strWidth = this.pickerSrv.mearsureStrWidth(str); + } + + private formatDateToString(date: Date): string { + if (!date) { + return ''; + } + return this.datepickerConvert.format(date, this.curFormat); + } + + private setI18nText() { + this.i18nLocale = this.i18n.getI18nText().locale; + this.i18n.langChange().pipe( + takeUntil(this.unsubscribe$) + ).subscribe((data) => { + this.i18nLocale = data.locale; + this.i18nText = data.datePickerPro; + }); + } + + inputChangeCallback = (type) => { + const targetValue = type === 'start' ? this.dateValue[0] : this.dateValue[1]; + if (!targetValue) { + return; + } + const inputDate = this.datepickerConvert.parse(targetValue, this.curFormat); + const curDate = type === 'start' ? this.pickerSrv.curRangeDate[0] : this.pickerSrv.curRangeDate[1]; + if (inputDate instanceof Date && inputDate.getTime() === curDate?.getTime()) { + return; + } + + if (this.validateDate(targetValue)) { + if (type === 'start') { + this.pickerSrv.curRangeDate[0] = inputDate; + } else { + this.pickerSrv.curRangeDate[1] = inputDate; + } + + this.pickerSrv.updateDateValue.next({ + type: 'range', + value: this.pickerSrv.curRangeDate + }); + + if (this.showTime) { + this.pickerSrv.updateTimeChange.next({ + activeInput: type, + hour: inputDate.getHours(), + min: inputDate.getMinutes(), + seconds: inputDate.getSeconds() + }); + } + } + + } + + inputBlurCallback = (type) => { + const targetValue = type === 'start' ? this.dateValue[0] : this.dateValue[1]; + if (!this.validateDate(targetValue)) { + if (type === 'start') { + this.dateValue[0] = this.pickerSrv.curRangeDate[0] ? + this.datepickerConvert.format(this.pickerSrv.curRangeDate[0], this.curFormat, this.locale || this.i18nLocale) : + ''; + } else { + this.dateValue[1] = this.pickerSrv.curRangeDate[1] ? + this.datepickerConvert.format(this.pickerSrv.curRangeDate[1], this.curFormat, this.locale || this.i18nLocale) : + ''; + } + } + this.getStrWidth(); + } + + public focusChange(type: 'start' | 'end') { + this.currentActiveInput = type; + this.pickerSrv.activeInputChange.next(type); + if (type === 'start') { + setTimeout(() => { + this.datepickerInputStart?.nativeElement?.focus(); + }); + } else { + setTimeout(() => { + this.datepickerInputEnd?.nativeElement?.focus(); + }); + } + } + + validateDate(value: string) { + const valueDate = this.datepickerConvert.parse(value, this.curFormat); + const valueFormat = valueDate && !isNaN(valueDate.getTime()) && + this.datepickerConvert.format(valueDate, this.curFormat, this.locale || this.i18nLocale); + if ( + !valueDate || value !== valueFormat || + (value === valueFormat && !this.pickerSrv.dateInRange(valueDate)) + ) { + return false; + } else { + return true; + } + } + + onToggle(isOpen) { + if (isOpen !== this.isOpen || isOpen) { + this.dropdownToggle.emit(isOpen); + } + this.isOpen = isOpen; + this.pickerSrv.toggleEvent.next(isOpen); + } + + openDropdown(event: Event) { + if (this.isOpen) { + event.stopPropagation(); + } + + this.isOpen = true; + } + + clear(event?: MouseEvent) { + event?.stopPropagation(); + if (this.disabled) { + return; + } + this.pickerSrv.updateDateValue.next({ + type: 'range', + value: [] + }); + + this.pickerSrv.updateTimeChange.next({ + hour: null, + min: null, + seconds: null + }); + this.dateValue = []; + this.pickerSrv.curRangeDate = []; + this.currentActiveInput = 'start'; + this.onChange(this.pickerSrv.curRangeDate); + } + + writeValue(value: Date[]) { + if (!value || !value.length) { + return; + } + + if (value.find(t => !this.pickerSrv.dateInRange(t))) { + return; + } + + this.dateValue = value.map(d => { + return d ? this.datepickerConvert.format(d, this.curFormat) : ''; + }); + this.pickerSrv.curRangeDate = value; + this.pickerSrv.updateDateValue.next({ + type: 'range', + value + }); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + ngOnDestroy() { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } +} diff --git a/devui/datepicker/date-range-picker.directive.ts b/devui/datepicker/date-range-picker.directive.ts index b5be4cd4..e37d4c66 100644 --- a/devui/datepicker/date-range-picker.directive.ts +++ b/devui/datepicker/date-range-picker.directive.ts @@ -1,4 +1,5 @@ import { CdkOverlayOrigin, ConnectedOverlayPositionChange, VerticalConnectionPos } from '@angular/cdk/overlay'; +import { DOCUMENT } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -7,6 +8,7 @@ import { EventEmitter, forwardRef, HostListener, + Inject, Input, OnDestroy, OnInit, @@ -73,6 +75,7 @@ export class DateRangePickerDirective implements OnInit, ControlValueAccessor, O datepickerPosition: VerticalConnectionPos = 'bottom'; valueList = []; startAnimation = false; + document: Document; private onChange = (_: any) => null; private onTouched = () => null; @@ -162,11 +165,11 @@ export class DateRangePickerDirective implements OnInit, ControlValueAccessor, O if (!isOpen) { this.startAnimation = false; removeClassFromOrigin(this.elementRef); - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } else { setTimeout(() => { addClassToOrigin(this.elementRef); - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); this.startAnimation = true; this.cdr.detectChanges(); }); @@ -177,20 +180,21 @@ export class DateRangePickerDirective implements OnInit, ControlValueAccessor, O get isOpen() { return this._isOpen; } - constructor( private elementRef: ElementRef, private renderer: Renderer2, private datePickerConfig: DatePickerConfig, private i18n: I18nService, private cdr: ChangeDetectorRef, - private devConfigService: DevConfigService + private devConfigService: DevConfigService, + @Inject(DOCUMENT) private doc: any ) { this._dateConfig = datePickerConfig['dateConfig']; this.dateConverter = datePickerConfig['dateConfig'].dateConverter || new DefaultDateConverter(); this._minDate = this.minDate ? new Date(this.minDate) : new Date(this.dateConfig.min, 0, 1, 0, 0, 0); this._maxDate = this.maxDate ? new Date(this.maxDate) : new Date(this.dateConfig.max, 11, 31, 23, 59, 59); this.setI18nText(); + this.document = this.doc; } @HostListener('blur', ['$event']) @@ -416,7 +420,7 @@ export class DateRangePickerDirective implements OnInit, ControlValueAccessor, O this.i18nSubscription.unsubscribe(); } this.valueChangeSubscrip?.unsubscribe(); - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } clearAll = (reason?: SelectDateRangeChangeReason, hide?: boolean) => { diff --git a/devui/datepicker/datepicker-cdk-overlay.component.ts b/devui/datepicker/datepicker-cdk-overlay.component.ts index 4bdf891d..1d3c8621 100755 --- a/devui/datepicker/datepicker-cdk-overlay.component.ts +++ b/devui/datepicker/datepicker-cdk-overlay.component.ts @@ -1,4 +1,5 @@ import { CdkOverlayOrigin, ConnectedOverlayPositionChange, ConnectedPosition, VerticalConnectionPos } from '@angular/cdk/overlay'; +import { DOCUMENT } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -6,6 +7,7 @@ import { EventEmitter, forwardRef, HostListener, + Inject, Input, OnChanges, OnDestroy, @@ -91,6 +93,7 @@ export class DatePickerAppendToBodyComponent implements OnInit, OnChanges, OnDes public i18nLocale: I18nInterface['locale']; public cdkConnectedOverlayOrigin: any; + document: Document; private onChange = (_: any) => null; private onTouched = () => null; @@ -153,13 +156,13 @@ export class DatePickerAppendToBodyComponent implements OnInit, OnChanges, OnDes if (!open) { this.startAnimation = false; removeClassFromOrigin(this.elementRef); - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } else { setTimeout(() => { this.startAnimation = true; this.cdr.detectChanges(); addClassToOrigin(this.elementRef); - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); }); } } @@ -171,10 +174,12 @@ export class DatePickerAppendToBodyComponent implements OnInit, OnChanges, OnDes constructor(private elementRef: ElementRef, private viewContainerRef: ViewContainerRef, private renderer2: Renderer2, private datePickerConfig: DatePickerConfig, private i18n: I18nService, - private cdr: ChangeDetectorRef) { + private cdr: ChangeDetectorRef, + @Inject(DOCUMENT) private doc: any) { this._dateConfig = datePickerConfig['dateConfig']; this.dateConverter = datePickerConfig['dateConfig'].dateConverter || new DefaultDateConverter(); this.setI18nText(); + this.document = this.doc; } @HostListener('blur', ['$event']) @@ -404,6 +409,6 @@ export class DatePickerAppendToBodyComponent implements OnInit, OnChanges, OnDes if (this.userInputSubscription) { this.userInputSubscription.unsubscribe(); } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } } diff --git a/devui/datepicker/datepicker.directive.ts b/devui/datepicker/datepicker.directive.ts index 8e9f884f..ea22462c 100755 --- a/devui/datepicker/datepicker.directive.ts +++ b/devui/datepicker/datepicker.directive.ts @@ -1,19 +1,33 @@ +import { animate, AnimationBuilder, AnimationMetadata, AnimationPlayer, style } from '@angular/animations'; +import { DOCUMENT } from '@angular/common'; import { - animate, - AnimationBuilder, - AnimationMetadata, - AnimationPlayer, - style -} from '@angular/animations'; -import { - ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, forwardRef, HostListener, Injector, - Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewContainerRef + ChangeDetectorRef, + ComponentFactoryResolver, + ComponentRef, + Directive, + ElementRef, + EventEmitter, + forwardRef, + HostListener, + Inject, + Injector, + Input, + OnDestroy, + OnInit, + Output, + Renderer2, + TemplateRef, + ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { I18nInterface, I18nService } from 'ng-devui/i18n'; import { - addClassToOrigin, AnimationCurves, AnimationDuration, - DateConverter, DefaultDateConverter, removeClassFromOrigin + addClassToOrigin, + AnimationCurves, + AnimationDuration, + DateConverter, + DefaultDateConverter, + removeClassFromOrigin } from 'ng-devui/utils'; import { DevConfigService, WithConfig } from 'ng-devui/utils/globalConfig'; import { fromEvent, Observable, Subscription } from 'rxjs'; @@ -25,11 +39,13 @@ import { DatepickerComponent } from './datepicker.component'; @Directive({ selector: '[dDatepicker]:not([appendToBody])', exportAs: 'datepicker', - providers: [{ - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DatepickerDirective), - multi: true - }] + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatepickerDirective), + multi: true, + }, + ], }) export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAccessor { @Input() locale: string; @@ -56,6 +72,7 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces private userInputSubscription: Subscription; private i18nSubscription: Subscription; private i18nLocale: I18nInterface['locale']; + document: Document; private onChange = (_: any) => null; private onTouched = () => null; @@ -65,11 +82,11 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces if (val) { addClassToOrigin(this.elementRef); setTimeout(() => { - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); }); } else { removeClassFromOrigin(this.elementRef); - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } } get isOpen() { @@ -130,17 +147,26 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces return this._minDate; } - constructor(private elementRef: ElementRef, private viewContainerRef: ViewContainerRef, - private componentFactoryResolver: ComponentFactoryResolver, private renderer2: Renderer2, - private injector: Injector, private datePickerConfig: DatePickerConfig, private i18n: I18nService, - private builder: AnimationBuilder, private cdr: ChangeDetectorRef, - private devConfigService: DevConfigService) { + constructor( + private elementRef: ElementRef, + private viewContainerRef: ViewContainerRef, + private componentFactoryResolver: ComponentFactoryResolver, + private renderer2: Renderer2, + private injector: Injector, + private datePickerConfig: DatePickerConfig, + private i18n: I18nService, + private builder: AnimationBuilder, + private cdr: ChangeDetectorRef, + private devConfigService: DevConfigService, + @Inject(DOCUMENT) private doc: any + ) { this._dateConfig = datePickerConfig['dateConfig']; this.dateConverter = datePickerConfig['dateConfig'].dateConverter || new DefaultDateConverter(); this.selectedDate = null; const factory = this.componentFactoryResolver.resolveComponentFactory(DatepickerComponent); this.cmpRef = this.viewContainerRef.createComponent(factory, this.viewContainerRef.length, this.injector); this.setI18nText(); + this.document = this.doc; } @HostListener('blur', ['$event']) @@ -153,8 +179,10 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces } checkDateConfig(dateConfig: any) { - if (!dateConfig) { return false; } - if (typeof (dateConfig.timePicker) !== 'boolean' || !dateConfig.max || !dateConfig.min || !dateConfig.format) { + if (!dateConfig) { + return false; + } + if (typeof dateConfig.timePicker !== 'boolean' || !dateConfig.max || !dateConfig.min || !dateConfig.format) { return false; } return true; @@ -171,14 +199,13 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces this.fillPopupData(); component.ngOnInit(); - component.registerOnChange(selectedDateObj => { + component.registerOnChange((selectedDateObj) => { this.writeValue(selectedDateObj); this.onChange(selectedDateObj.selectedDate); }); component.selectedDateChange.subscribe((arg: SelectDateChangeEventArgs) => { - if (arg.reason === SelectDateChangeReason.date && !this.showTime || - arg.reason === SelectDateChangeReason.button) { + if ((arg.reason === SelectDateChangeReason.date && !this.showTime) || arg.reason === SelectDateChangeReason.button) { this.hide(); } }); @@ -197,8 +224,7 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces } else { curDate = obj; } - this.selectedDate = curDate ? - this.dateConverter.parse(curDate) : null; + this.selectedDate = curDate ? this.dateConverter.parse(curDate) : null; this.writeModelValue(obj); } @@ -266,7 +292,7 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces if (selectDateObj && typeof selectDateObj === 'object' && selectDateObj.hasOwnProperty('selectedDate')) { this.selectedDateChange.emit({ reason: dateReason, - selectedDate: this.selectedDate + selectedDate: this.selectedDate, }); this.onTouched(); } @@ -302,13 +328,24 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces } private fillPopupData() { - ['showTime', 'maxDate', 'minDate', 'cssClass', 'disabled', 'dateConverter', 'locale', 'dateFormat', 'yearNumber', 'dateConfig', 'mode', - 'customViewTemplate'] - .forEach(key => { - if (this[key] !== undefined) { - this.cmpRef.instance[key] = this[key]; - } - }); + [ + 'showTime', + 'maxDate', + 'minDate', + 'cssClass', + 'disabled', + 'dateConverter', + 'locale', + 'dateFormat', + 'yearNumber', + 'dateConfig', + 'mode', + 'customViewTemplate', + ].forEach((key) => { + if (this[key] !== undefined) { + this.cmpRef.instance[key] = this[key]; + } + }); } onDocumentClick = ($event) => { @@ -331,15 +368,19 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces case 'top': return [ style({ transform: 'scaleY(0.8) translateY(4px)', opacity: 0.8, transformOrigin: '0% 100%', display: 'block' }), - animate(`${AnimationDuration.BASE} ${AnimationCurves.EASE_OUT}`, - style({ transform: 'scaleY(0.9999) translateY(0)', opacity: 1, transformOrigin: '0% 100%', display: 'block' })), + animate( + `${AnimationDuration.BASE} ${AnimationCurves.EASE_OUT}`, + style({ transform: 'scaleY(0.9999) translateY(0)', opacity: 1, transformOrigin: '0% 100%', display: 'block' }) + ), ]; case 'bottom': default: return [ style({ transform: 'scaleY(0.8) translateY(-4px)', opacity: 0.8, transformOrigin: '0% 0%', display: 'block' }), - animate(`${AnimationDuration.BASE} ${AnimationCurves.EASE_OUT}`, - style({ transform: 'scaleY(0.9999) translateY(0)', opacity: 1, transformOrigin: '0% 0%', display: 'block' })), + animate( + `${AnimationDuration.BASE} ${AnimationCurves.EASE_OUT}`, + style({ transform: 'scaleY(0.9999) translateY(0)', opacity: 1, transformOrigin: '0% 0%', display: 'block' }) + ), ]; } } @@ -349,15 +390,19 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces case 'top': return [ style({ transform: 'scaleY(0.9999) translateY(0)', opacity: 1, transformOrigin: '0% 100%', display: 'block' }), - animate(`${AnimationDuration.BASE} ${AnimationCurves.EASE_IN}`, - style({ transform: 'scaleY(0.8) translateY(4px)', opacity: 0.8, transformOrigin: '0% 100%', display: 'block' })) + animate( + `${AnimationDuration.BASE} ${AnimationCurves.EASE_IN}`, + style({ transform: 'scaleY(0.8) translateY(4px)', opacity: 0.8, transformOrigin: '0% 100%', display: 'block' }) + ), ]; case 'bottom': default: return [ style({ transform: 'scaleY(0.9999) translateY(0)', opacity: 1, transformOrigin: '0% 0%', display: 'block' }), - animate(`${AnimationDuration.BASE} ${AnimationCurves.EASE_IN}`, - style({ transform: 'scaleY(0.8) translateY(-4px)', opacity: 0.8, transformOrigin: '0% 0%', display: 'block' })) + animate( + `${AnimationDuration.BASE} ${AnimationCurves.EASE_IN}`, + style({ transform: 'scaleY(0.8) translateY(-4px)', opacity: 0.8, transformOrigin: '0% 0%', display: 'block' }) + ), ]; } } @@ -403,7 +448,9 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces return; } const valueDate = new Date(value); - const valueFormat = valueDate instanceof Date && !isNaN(valueDate.getTime()) && + const valueFormat = + valueDate instanceof Date && + !isNaN(valueDate.getTime()) && this.dateConverter.format(valueDate, this.dateFormat, this.locale || this.i18nLocale); if (new Date(valueFormat).getTime() === new Date(this.selectedDate).getTime() || !this.validateDate(value)) { return; @@ -417,10 +464,11 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces validateDate(value: string) { const valueDate = new Date(value); - const valueFormat = valueDate && !isNaN(valueDate.getTime()) && - this.dateConverter.format(valueDate, this.dateFormat, this.locale || this.i18nLocale); + const valueFormat = + valueDate && !isNaN(valueDate.getTime()) && this.dateConverter.format(valueDate, this.dateFormat, this.locale || this.i18nLocale); if ( - !valueDate || value !== valueFormat || + !valueDate || + value !== valueFormat || (value === valueFormat && (valueDate.getTime() < this.minDate.getTime() || valueDate.getTime() > this.maxDate.getTime())) ) { return false; @@ -430,9 +478,7 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces } resetValue() { - const resDate = this.selectedDate ? - this.dateConverter.format(this.selectedDate, this.dateFormat, this.locale || this.i18nLocale) : - ''; + const resDate = this.selectedDate ? this.dateConverter.format(this.selectedDate, this.dateFormat, this.locale || this.i18nLocale) : ''; this.elementRef.nativeElement.value = resDate; } @@ -443,6 +489,6 @@ export class DatepickerDirective implements OnInit, OnDestroy, ControlValueAcces if (this.userInputSubscription) { this.userInputSubscription.unsubscribe(); } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } } diff --git a/devui/datepicker/two-datepicker/two-datepicker.component.ts b/devui/datepicker/two-datepicker/two-datepicker.component.ts index af5eddc7..c7ffb724 100644 --- a/devui/datepicker/two-datepicker/two-datepicker.component.ts +++ b/devui/datepicker/two-datepicker/two-datepicker.component.ts @@ -1,9 +1,11 @@ import { CdkOverlayOrigin, ConnectedOverlayPositionChange, VerticalConnectionPos } from '@angular/cdk/overlay'; +import { DOCUMENT } from '@angular/common'; import { ChangeDetectorRef, Component, ElementRef, EventEmitter, + Inject, Input, OnDestroy, OnInit, @@ -63,6 +65,7 @@ export class TwoDatePickerComponent implements OnInit, OnDestroy { private _dateFormat: string; private _maxDate: Date; private _minDate: Date; + document: Document; @Input() set dateConfig(dateConfig: any) { if (this.checkDateConfig(dateConfig)) { @@ -97,12 +100,12 @@ export class TwoDatePickerComponent implements OnInit, OnDestroy { this._isOpen = isOpen; if (!isOpen) { this.startAnimation = false; - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } else { setTimeout(() => { this.startAnimation = true; this.cdr.detectChanges(); - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); }); } } @@ -131,11 +134,13 @@ export class TwoDatePickerComponent implements OnInit, OnDestroy { protected datePickerConfig: DatePickerConfig, private i18n: I18nService, private cdr: ChangeDetectorRef, - private devConfigService: DevConfigService + private devConfigService: DevConfigService, + @Inject(DOCUMENT) private doc: any ) { this._dateConfig = datePickerConfig['dateConfig']; this.dateConverter = datePickerConfig['dateConfig'].dateConverter || new DefaultDateConverter(); this.setI18nText(); + this.document = this.doc; } checkDateConfig(dateConfig: any) { @@ -347,6 +352,6 @@ export class TwoDatePickerComponent implements OnInit, OnDestroy { if (this.i18nSubscription) { this.i18nSubscription.unsubscribe(); } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } } diff --git a/devui/design-token/border-radius/demo/border-radius/border-radius.component.html b/devui/design-token/border-radius/demo/border-radius/border-radius.component.html index 814dbd3f..e93875e5 100644 --- a/devui/design-token/border-radius/demo/border-radius/border-radius.component.html +++ b/devui/design-token/border-radius/demo/border-radius/border-radius.component.html @@ -1,4 +1,4 @@ -

{{ 'components.design-border-radius.cornerDemo.instance.tableTitle' | translate }}

+

{{ 'components.design-border-radius.cornerDemo.instance.tableTitle' | translate }}

{ diff --git a/devui/design-token/color/demo/color/color.component.html b/devui/design-token/color/demo/color/color.component.html index f35577d8..1f73af80 100644 --- a/devui/design-token/color/demo/color/color.component.html +++ b/devui/design-token/color/demo/color/color.component.html @@ -1,5 +1,5 @@
-

{{ 'components.design-color.colorDemo.instance.title' | translate }}

+

{{ 'components.design-color.colorDemo.instance.title' | translate }}

{{ 'components.design-color.colorDemo.instance.description' | translate }}
diff --git a/devui/design-token/color/demo/color/color.component.ts b/devui/design-token/color/demo/color/color.component.ts index e8d8f2eb..51efb48f 100644 --- a/devui/design-token/color/demo/color/color.component.ts +++ b/devui/design-token/color/demo/color/color.component.ts @@ -28,11 +28,13 @@ export class ColorComponent implements OnInit, OnDestroy { constructor(private translate: TranslateService) {} ngOnInit() { - this.themeService = window['devuiThemeService']; - if (this.themeService.eventBus) { - this.themeService.eventBus.add('themeChanged', this.changeValueInTable); + if (typeof window !== undefined) { + this.themeService = window['devuiThemeService']; + if (this.themeService.eventBus) { + this.themeService.eventBus.add('themeChanged', this.changeValueInTable); + } + this.setI18n(); } - this.setI18n(); } changeValueInTable = () => { diff --git a/devui/design-token/font/demo/font/font.component.html b/devui/design-token/font/demo/font/font.component.html index cd317b36..4f65d045 100644 --- a/devui/design-token/font/demo/font/font.component.html +++ b/devui/design-token/font/demo/font/font.component.html @@ -1,4 +1,4 @@ -

{{ 'components.design-font.fontDemo.instance.title' | translate }}

+

{{ 'components.design-font.fontDemo.instance.title' | translate }}

{{ 'components.design-font.fontDemo.instance.description1' | translate }} @@ -7,7 +7,7 @@

{{ 'components.design-font.fontDemo.instance.title' | translate }}

{{ 'components.design-font.fontDemo.instance.description2' | translate }}
-

{{ 'components.design-font.fontDemo.instance.tableTitle' | translate }}

+

{{ 'components.design-font.fontDemo.instance.tableTitle' | translate }}

diff --git a/devui/design-token/font/demo/font/font.component.ts b/devui/design-token/font/demo/font/font.component.ts index 3f43c50b..bb9cb2ce 100644 --- a/devui/design-token/font/demo/font/font.component.ts +++ b/devui/design-token/font/demo/font/font.component.ts @@ -21,11 +21,13 @@ export class FontComponent implements OnInit, OnDestroy { constructor(private translate: TranslateService) {} ngOnInit() { - this.themeService = window['devuiThemeService']; - if (this.themeService.eventBus) { - this.themeService.eventBus.add('themeChanged', this.changeValueInTable); + if (typeof window !== undefined) { + this.themeService = window['devuiThemeService']; + if (this.themeService.eventBus) { + this.themeService.eventBus.add('themeChanged', this.changeValueInTable); + } + this.setI18n(); } - this.setI18n(); } changeValueInTable = () => { diff --git a/devui/design-token/shadow/demo/shadow/shadow.component.html b/devui/design-token/shadow/demo/shadow/shadow.component.html index a32ffd02..02e00ccc 100644 --- a/devui/design-token/shadow/demo/shadow/shadow.component.html +++ b/devui/design-token/shadow/demo/shadow/shadow.component.html @@ -1,7 +1,7 @@ -

{{ 'components.design-shadow.shadowDemo.instance.title' | translate }}

+

{{ 'components.design-shadow.shadowDemo.instance.title' | translate }}

-

{{ 'components.design-shadow.shadowDemo.instance.description' | translate }}

+

{{ 'components.design-shadow.shadowDemo.instance.description' | translate }}

diff --git a/devui/design-token/shadow/demo/shadow/shadow.component.ts b/devui/design-token/shadow/demo/shadow/shadow.component.ts index 399354f1..6a077c32 100644 --- a/devui/design-token/shadow/demo/shadow/shadow.component.ts +++ b/devui/design-token/shadow/demo/shadow/shadow.component.ts @@ -20,11 +20,13 @@ export class ShadowComponent implements OnInit, OnDestroy { constructor(private translate: TranslateService) {} ngOnInit() { - this.themeService = window['devuiThemeService']; - if (this.themeService.eventBus) { - this.themeService.eventBus.add('themeChanged', this.changeValueInTable); + if (typeof window !== undefined) { + this.themeService = window['devuiThemeService']; + if (this.themeService.eventBus) { + this.themeService.eventBus.add('themeChanged', this.changeValueInTable); + } + this.setI18n(); } - this.setI18n(); } changeValueInTable = () => { diff --git a/devui/devui.module.ts b/devui/devui.module.ts index c67d870f..8f92b296 100755 --- a/devui/devui.module.ts +++ b/devui/devui.module.ts @@ -16,6 +16,7 @@ import { CheckBoxModule } from 'ng-devui/checkbox'; import { DCommonModule } from 'ng-devui/common'; import { DataTableModule } from 'ng-devui/data-table'; import { DatepickerModule } from 'ng-devui/datepicker'; +import { DatepickerProModule } from 'ng-devui/datepicker-pro'; import { DragDropModule } from 'ng-devui/dragdrop'; import { DrawerModule } from 'ng-devui/drawer'; import { DropDownModule } from 'ng-devui/dropdown'; @@ -78,6 +79,7 @@ export * from 'ng-devui/checkbox'; export * from 'ng-devui/common'; export * from 'ng-devui/data-table'; export * from 'ng-devui/datepicker'; +export * from 'ng-devui/datepicker-pro'; export * from 'ng-devui/dragdrop'; export * from 'ng-devui/drawer'; export * from 'ng-devui/dropdown'; @@ -187,6 +189,7 @@ export * from './version'; TimePickerModule, CascaderModule, CategorySearchModule, + DatepickerProModule, MentionModule, NavSpriteModule, ReadTipModule, diff --git a/devui/dragdrop/demo/follow/follow.component.html b/devui/dragdrop/demo/follow/follow.component.html index 82099b09..6955c226 100755 --- a/devui/dragdrop/demo/follow/follow.component.html +++ b/devui/dragdrop/demo/follow/follow.component.html @@ -1,4 +1,4 @@ -

Drag Element with Real Entity(not ghost) followed

+

Drag Element with Real Entity(not ghost) followed

diff --git a/devui/dragdrop/demo/tree/tree.component.html b/devui/dragdrop/demo/tree/tree.component.html index 91fa32bf..08b713e5 100755 --- a/devui/dragdrop/demo/tree/tree.component.html +++ b/devui/dragdrop/demo/tree/tree.component.html @@ -1,4 +1,4 @@ -

Multi-level Tree Drag

+

Multi-level Tree Drag

diff --git a/devui/dragdrop/directives/draggable.directive.ts b/devui/dragdrop/directives/draggable.directive.ts index 6e089bbe..f77eaaa0 100755 --- a/devui/dragdrop/directives/draggable.directive.ts +++ b/devui/dragdrop/directives/draggable.directive.ts @@ -1,5 +1,8 @@ -import { AfterViewInit, Directive, ElementRef, EventEmitter, Host, - HostBinding, Input, NgZone, OnDestroy, OnInit, Optional, Output, Renderer2, Self } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { + AfterViewInit, Directive, ElementRef, EventEmitter, + HostBinding, Inject, Input, NgZone, OnDestroy, OnInit, Optional, Output, Renderer2, Self +} from '@angular/core'; import { fromEvent, Subject, Subscription } from 'rxjs'; import { DragDropService } from '../services/drag-drop.service'; import { Utils } from '../shared/utils'; @@ -99,10 +102,12 @@ export class DraggableDirective implements OnInit, AfterViewInit, OnDestroy { private dragOriginPlaceholderNextSibling; public dragElShowHideEvent = new Subject(); public beforeDragStartEvent = new Subject(); + document: Document; constructor(public el: ElementRef, private renderer: Renderer2, private dragDropService: DragDropService, private ngZone: NgZone, - @Optional() @Self() public dragPreviewDirective: DragPreviewDirective + @Optional() @Self() public dragPreviewDirective: DragPreviewDirective, @Inject(DOCUMENT) private doc: any ) { + this.document = this.doc; } ngOnInit() { @@ -377,7 +382,7 @@ export class DraggableDirective implements OnInit, AfterViewInit, OnDestroy { this.delayRemoveOriginPlaceholderTimer = undefined; } - const node = document.createElement(this.originPlaceholder.tag || 'div'); + const node = this.document.createElement(this.originPlaceholder.tag || 'div'); const rect = this.el.nativeElement.getBoundingClientRect(); if (directShow) { node.style.display = 'block'; @@ -445,7 +450,7 @@ export class DraggableDirective implements OnInit, AfterViewInit, OnDestroy { delayOriginPlaceholder.classList.add('delay-deletion'); this.delayRemoveOriginPlaceholderTimer = setTimeout(() => { delayOriginPlaceholder.parentElement.removeChild(delayOriginPlaceholder); - if (document.body.contains(this.el.nativeElement)) { + if (this.document.body.contains(this.el.nativeElement)) { this.el.nativeElement.style.display = ''; this.dragDropService.dragElShowHideEvent.next(false); } @@ -475,7 +480,7 @@ export class DraggableDirective implements OnInit, AfterViewInit, OnDestroy { if (!element.parentNode) { return null; } // 模拟一个元素测预测位置和最终位置是否符合,如果不符合则是有transform等造成的偏移 const elementPosition = element.getBoundingClientRect(); - const testEl = document.createElement('div'); + const testEl = this.document.createElement('div'); Utils.addElStyles(testEl, { opacity: '0', position: 'fixed', diff --git a/devui/dragdrop/directives/drop-scroll-enhance.directive.ts b/devui/dragdrop/directives/drop-scroll-enhance.directive.ts index 6fb81cce..faeba053 100644 --- a/devui/dragdrop/directives/drop-scroll-enhance.directive.ts +++ b/devui/dragdrop/directives/drop-scroll-enhance.directive.ts @@ -1,10 +1,13 @@ -import { AfterViewInit, Directive, ElementRef, Input, NgZone, OnDestroy } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Directive, ElementRef, Inject, Input, NgZone, OnDestroy } from '@angular/core'; import { fromEvent, merge as mergeStatic, Subscription } from 'rxjs'; import { tap, throttleTime } from 'rxjs/operators'; import { DragDropService } from '../services/drag-drop.service'; import { Utils } from '../shared/utils'; -import { DropScrollAreaOffset, DropScrollDirection, DropScrollEnhanceTimingFunctionGroup, DropScrollOrientation, - DropScrollSpeed, DropScrollSpeedFunction, DropScrollTriggerEdge } from './drop-scroll-enhance.type'; +import { + DropScrollAreaOffset, DropScrollDirection, DropScrollEnhanceTimingFunctionGroup, DropScrollOrientation, + DropScrollSpeed, DropScrollSpeedFunction, DropScrollTriggerEdge +} from './drop-scroll-enhance.type'; @Directive({ selector: '[dDropScrollEnhanced]', @@ -30,8 +33,12 @@ export class DropScrollEnhancedDirective implements AfterViewInit, OnDestroy { private backwardScrollFn: (event: DragEvent) => void; private lastScrollTime; private animationFrameId: number; + document: Document; - constructor(private el: ElementRef, private zone: NgZone, private dragDropService: DragDropService) {} + constructor(private el: ElementRef, private zone: NgZone, private dragDropService: DragDropService, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } ngAfterViewInit() { // 设置父元素 @@ -116,6 +123,9 @@ export class DropScrollEnhancedDirective implements AfterViewInit, OnDestroy { createScrollFn(direction: DropScrollDirection, orientation: DropScrollOrientation, speedFn: DropScrollSpeedFunction) { + if (typeof window === 'undefined') { + return; + } const scrollAttr = (direction === 'v') ? 'scrollTop' : 'scrollLeft'; const eventAttr = (direction === 'v') ? 'clientY' : 'clientX'; const scrollWidthAttr = (direction === 'v') ? 'scrollHeight' : 'scrollWidth'; @@ -182,7 +192,7 @@ export class DropScrollEnhancedDirective implements AfterViewInit, OnDestroy { } createScrollArea(direction: DropScrollDirection, orientation: DropScrollOrientation) { - const area = document.createElement('div'); + const area = this.document.createElement('div'); area.className = 'dropover-scroll-area ' + 'dropover-scroll-area-' + this.getCriticalEdge(direction, orientation); // 处理大小 area.classList.add('active'); @@ -236,6 +246,9 @@ export class DropScrollEnhancedDirective implements AfterViewInit, OnDestroy { } getRelatedPosition(target, relatedTarget, edge: DropScrollTriggerEdge, offsetValue?: number) { + if (typeof window === 'undefined') { + return '0px'; + } const relatedComputedStyle = window.getComputedStyle(relatedTarget); const relatedRect = relatedTarget.getBoundingClientRect(); const selfRect = target.getBoundingClientRect(); @@ -288,7 +301,7 @@ export class DropScrollEnhancedDirective implements AfterViewInit, OnDestroy { } cleanLastScrollTime() { - if (this.animationFrameId) { + if (this.animationFrameId && typeof window !== 'undefined') { window.cancelAnimationFrame(this.animationFrameId); this.animationFrameId = undefined; } diff --git a/devui/dragdrop/directives/droppable.directive.ts b/devui/dragdrop/directives/droppable.directive.ts index 299d7f43..dd992b40 100755 --- a/devui/dragdrop/directives/droppable.directive.ts +++ b/devui/dragdrop/directives/droppable.directive.ts @@ -1,9 +1,11 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Directive, ElementRef, EventEmitter, HostListener, + Inject, Input, NgZone, OnDestroy, @@ -123,13 +125,15 @@ export class DroppableDirective implements OnInit, AfterViewInit, OnDestroy { /* 协同拖拽需要 */ placeholderInsertionEvent = new Subject(); placeholderRenderEvent = new Subject(); + document: Document; - constructor(protected el: ElementRef, private renderer: Renderer2, private dragDropService: DragDropService, private ngZone: NgZone) { - + constructor(protected el: ElementRef, private renderer: Renderer2, private dragDropService: DragDropService, private ngZone: NgZone, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit() { - this.placeholder = document.createElement(this.placeholderTag); + this.placeholder = this.document.createElement(this.placeholderTag); this.placeholder.className = 'drag-placeholder'; this.placeholder.innerText = this.placeholderText; this.dragStartSubscription = this.dragDropService.dragStartEvent.subscribe(() => this.setPlaceholder()); diff --git a/devui/dragdrop/services/drag-drop.service.ts b/devui/dragdrop/services/drag-drop.service.ts index 4103b484..8bb54a9f 100755 --- a/devui/dragdrop/services/drag-drop.service.ts +++ b/devui/dragdrop/services/drag-drop.service.ts @@ -1,5 +1,6 @@ -import { Injectable, NgZone } from '@angular/core'; -import { Observable, Subject, Subscription } from 'rxjs'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, NgZone } from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; import { DragPreviewDirective } from '../directives/drag-preview.directive'; import { Utils } from '../shared/utils'; import { DragDropTouch } from '../touch-support/dragdrop-touch'; @@ -55,12 +56,14 @@ export class DragDropService { dragSyncGroupDirectives; /*预览功能 */ dragPreviewDirective: DragPreviewDirective; + document: Document; - constructor(private ngZone: NgZone) { + constructor(private ngZone: NgZone, @Inject(DOCUMENT) private doc: any) { this.touchInstance = DragDropTouch.getInstance(); // service not support OnInit, only support OnDestroy, so write in constructor // tslint:disable-next-line: max-line-length this.dragEmptyImage.src = 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=='; // safari的img必须要有src + this.document = this.doc; } newSubscription() { this.subscription.unsubscribe(); @@ -73,19 +76,19 @@ export class DragDropService { if (this.dragPreviewDirective && this.dragPreviewDirective.dragPreviewTemplate) { this.dragPreviewDirective.createPreview(); this.dragCloneNode = this.dragPreviewDirective.getPreviewElement(); - this.dragItemContainer = document.body; + this.dragItemContainer = this.document.body; } else { this.dragCloneNode = this.draggedEl.cloneNode(true); } this.dragCloneNode.style.margin = '0'; if (this.dragFollowOptions && this.dragFollowOptions.appendToBody) { - this.dragItemContainer = document.body; + this.dragItemContainer = this.document.body; this.copyStyle(this.draggedEl, this.dragCloneNode); } if (this.dragItemChildrenName !== '') { - const parentElement = this.dragItemParentName === '' ? this.dragCloneNode : document.querySelector(this.dragItemParentName); + const parentElement = this.dragItemParentName === '' ? this.dragCloneNode : this.document.querySelector(this.dragItemParentName); const dragItemChildren = parentElement.querySelectorAll(this.dragItemChildrenName); this.interceptChildNode(parentElement, dragItemChildren); } @@ -97,7 +100,7 @@ export class DragDropService { }); this.ngZone.runOutsideAngular(() => { - document.addEventListener('dragover', this.followMouse4CloneNode, { capture: true, passive: true }); + this.document.addEventListener('dragover', this.followMouse4CloneNode, { capture: true, passive: true }); }); this.dragCloneNode.style.width = this.dragOffset.width + 'px'; this.dragCloneNode.style.height = this.dragOffset.height + 'px'; @@ -109,13 +112,13 @@ export class DragDropService { // 批量拖拽样式 if (this.batchDragging && this.batchDragData && this.batchDragData.length > 1) { // 创建一个节点容器 - const node = document.createElement('div'); + const node = this.document.createElement('div'); node.appendChild(this.dragCloneNode); node.classList.add('batch-dragged-node'); /* 计数样式定位 */ if (this.batchDragStyle && this.batchDragStyle.length && this.batchDragStyle.indexOf('badge') > -1) { - const badge = document.createElement('div'); + const badge = this.document.createElement('div'); badge.innerText = this.batchDragData.length + ''; badge.classList.add('batch-dragged-node-count'); node.style.position = 'relative'; @@ -190,7 +193,7 @@ export class DragDropService { disableDraggedCloneNodeFollowMouse() { if (this.dragCloneNode) { - document.removeEventListener('dragover', this.followMouse4CloneNode, { capture: true }); + this.document.removeEventListener('dragover', this.followMouse4CloneNode, { capture: true }); this.dragItemContainer.removeChild(this.dragCloneNode); this.draggedEl.style.display = ''; this.dragElShowHideEvent.next(true); diff --git a/devui/dragdrop/touch-support/dragdrop-touch.ts b/devui/dragdrop/touch-support/dragdrop-touch.ts index 9e7e0303..fc59fc36 100644 --- a/devui/dragdrop/touch-support/dragdrop-touch.ts +++ b/devui/dragdrop/touch-support/dragdrop-touch.ts @@ -50,30 +50,31 @@ export class DragDropTouch { // detect passive event support // https://github.com/Modernizr/Modernizr/issues/1894 let supportsPassive = false; - document.addEventListener('test', () => {}, { - get passive() { - supportsPassive = true; - return true; + if (typeof document !== 'undefined') { + document.addEventListener('test', () => {}, { + get passive() { + supportsPassive = true; + return true; + } + }); + // listen to touch events + if (DragDropTouch.isTouchDevice()) { + // 能响应触摸事件 + const d = document; + const ts = this.touchstart; + const tmod = this.touchmoveOnDocument; + const teod = this.touchendOnDocument; + const opt = supportsPassive ? { passive: false, capture: false } : false; + const optPassive = supportsPassive ? {passive: true} : false; + d.addEventListener('touchstart', ts, opt); + d.addEventListener('touchmove', tmod, optPassive); + d.addEventListener('touchend', teod); + d.addEventListener('touchcancel', teod); + this.touchmoveListener = this.touchmove; + this.touchendListener = this.touchend; + this.listenerOpt = opt; } - }); - // listen to touch events - if (DragDropTouch.isTouchDevice()) { - // 能响应触摸事件 - const d = document; - const ts = this.touchstart; - const tmod = this.touchmoveOnDocument; - const teod = this.touchendOnDocument; - const opt = supportsPassive ? { passive: false, capture: false } : false; - const optPassive = supportsPassive ? {passive: true} : false; - d.addEventListener('touchstart', ts, opt); - d.addEventListener('touchmove', tmod, optPassive); - d.addEventListener('touchend', teod); - d.addEventListener('touchcancel', teod); - this.touchmoveListener = this.touchmove; - this.touchendListener = this.touchend; - this.listenerOpt = opt; } - } /** * Gets a reference to the @see:DragDropTouch singleton. @@ -85,6 +86,9 @@ export class DragDropTouch { return DragDropTouch.instance; } static isTouchDevice() { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return false; + } const d: Document = document; const w: Window = window; let bool; diff --git a/devui/drawer/demo/drawer-demo.component.html b/devui/drawer/demo/drawer-demo.component.html index 44ccaf24..dac878b3 100755 --- a/devui/drawer/demo/drawer-demo.component.html +++ b/devui/drawer/demo/drawer-demo.component.html @@ -14,4 +14,11 @@
+
+
{{ 'components.drawer.templateDemo.title' | translate }}
+
{{ 'components.drawer.templateDemo.description' | translate }}
+ + + +
diff --git a/devui/drawer/demo/drawer-demo.component.ts b/devui/drawer/demo/drawer-demo.component.ts index fcb0c4d5..73451e50 100755 --- a/devui/drawer/demo/drawer-demo.component.ts +++ b/devui/drawer/demo/drawer-demo.component.ts @@ -23,6 +23,11 @@ export class DrawerDemoComponent implements OnInit, OnDestroy { { title: 'drawerContent-css', language: 'css', code: require('!!raw-loader!./drawerContent/drawer-content.component.scss') }, ]; + templateSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./template/template.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./template/template.component.ts') } + ]; + navItems = []; subs: Subscription = new Subscription(); constructor(private translate: TranslateService) {} @@ -46,6 +51,7 @@ export class DrawerDemoComponent implements OnInit, OnDestroy { this.navItems = [ { dAnchorLink: 'basic-usage', value: values['basic-usage'] }, { dAnchorLink: 'do-not-destroy-after-closing', value: values['do-not-destroy-after-closing'] }, + { dAnchorLink: 'template', value: values['template'] } ]; } diff --git a/devui/drawer/demo/drawer-demo.module.ts b/devui/drawer/demo/drawer-demo.module.ts index 433cdea0..091d9ea0 100755 --- a/devui/drawer/demo/drawer-demo.module.ts +++ b/devui/drawer/demo/drawer-demo.module.ts @@ -13,6 +13,7 @@ import { DDemoNavModule } from 'src/app/component/d-demo-nav.module'; import { BasicComponent } from './basic/basic.component'; import { DrawerDemoComponent } from './drawer-demo.component'; import { DrawerContentComponent } from './drawerContent/drawer-content.component'; +import { TemplateComponent } from './template/template.component'; import { UndestroyableComponent } from './undestroyable/undestroyable.component'; @NgModule({ @@ -40,7 +41,7 @@ import { UndestroyableComponent } from './undestroyable/undestroyable.component' ]), ], exports: [], - declarations: [DrawerDemoComponent, BasicComponent, UndestroyableComponent, DrawerContentComponent], + declarations: [DrawerDemoComponent, BasicComponent, UndestroyableComponent, DrawerContentComponent, TemplateComponent], }) export class DrawerDemoModule {} diff --git a/devui/drawer/demo/template/template.component.html b/devui/drawer/demo/template/template.component.html new file mode 100644 index 00000000..53ddf793 --- /dev/null +++ b/devui/drawer/demo/template/template.component.html @@ -0,0 +1,11 @@ +click me! + + +
+ +
+
+
Hello, Drawer!
+
Powered by template.
+
+
diff --git a/devui/drawer/demo/template/template.component.ts b/devui/drawer/demo/template/template.component.ts new file mode 100644 index 00000000..0be341f0 --- /dev/null +++ b/devui/drawer/demo/template/template.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { DrawerService, IDrawerOpenResult } from 'ng-devui/drawer'; + +@Component({ + selector: 'd-template', + templateUrl: './template.component.html' +}) +export class TemplateComponent implements OnInit { + @ViewChild('drawerContent', { static: true }) drawerContent: TemplateRef; + results: IDrawerOpenResult; + constructor(private drawerService: DrawerService) { } + + ngOnInit() { + } + + openDrawer() { + this.results = this.drawerService.open({ + width: '300px', + zIndex: 1000, + isCover: true, + fullScreen: true, + backdropCloseable: true, + escKeyCloseable: true, + position: 'left', + onClose: () => { + console.log('on drawer closed'); + }, + contentTemplate: this.drawerContent + }); + } + + close($event) { + this.results.drawerInstance.hide(); + } +} diff --git a/devui/drawer/demo/undestroyable/undestroyable.component.html b/devui/drawer/demo/undestroyable/undestroyable.component.html index 3ed0abd2..3224de8c 100755 --- a/devui/drawer/demo/undestroyable/undestroyable.component.html +++ b/devui/drawer/demo/undestroyable/undestroyable.component.html @@ -1,6 +1,6 @@
Open
- Manual Destroy + Manual Destroy
diff --git a/devui/drawer/demo/undestroyable/undestroyable.component.ts b/devui/drawer/demo/undestroyable/undestroyable.component.ts index a966488b..9462054c 100755 --- a/devui/drawer/demo/undestroyable/undestroyable.component.ts +++ b/devui/drawer/demo/undestroyable/undestroyable.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject } from '@angular/core'; import { DrawerService, IDrawerOpenResult } from 'ng-devui/drawer'; import { DrawerContentComponent } from '../drawerContent/drawer-content.component'; @@ -10,8 +11,8 @@ export class UndestroyableComponent { results: IDrawerOpenResult; doms: any = []; - constructor(private drawerService: DrawerService) { - this.doms.push(document.getElementById('app-container')); + constructor(private drawerService: DrawerService, @Inject(DOCUMENT) private doc: any) { + this.doms.push(this.doc.getElementById('app-container')); } destroy() { diff --git a/devui/drawer/doc/api-cn.md b/devui/drawer/doc/api-cn.md index f294bd34..7fffbb40 100644 --- a/devui/drawer/doc/api-cn.md +++ b/devui/drawer/doc/api-cn.md @@ -44,7 +44,8 @@ openDrawer() { | 属性 | 类型 | 默认 | 说明 | 跳转 Demo |全局配置项| | :----------------: | :----------------------: | :-------------------------------------------: | :-----: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------: | ------------------------------------------------- | -| drawerContentComponent | `Type` | -- | 必要参数,传入自定义的 component | [基本用法](demo#basic-usage) | +| drawerContentComponent | `Type` | -- | 可选,传入自定义的 component | [基本用法](demo#basic-usage) | +| contentTemplate | `TemplateRef` | -- | 可选,自定义模板,与drawerContentComponent不兼容 | [自定义模板](demo#template) | | componentFactoryResolver | `ComponentFactoryResolver` | 内置 | 可选,一般不需要设置 | | injector | `Injector` | -- | 可选,一般不需要设置 | | width | `string` | '300px' | 可选,设置 drawer 的宽度 | [基本用法](demo#basic-usage) | @@ -54,7 +55,7 @@ openDrawer() { | backdropCloseable | `boolean` | true | 可选,设置可否通过点击背景来关闭 drawer 层 | [基本用法](demo#basic-usage) | | escKeyCloseable | `boolean` | true | 可选,设置可否通过 esc 按键来关闭 drawer 层 | [基本用法](demo#basic-usage) | | onClose | `Function` | -- | 可选,关闭 drawer 时候调用 | [基本用法](demo#basic-usage) | -| afterOpened | `Function` | -- | \可选,打开 drawer 后时候调用 | +| afterOpened | `Function` | -- | 可选,打开 drawer 后时候调用 | | beforeHidden | `Function\|Promise\|()=> Observable` | -- | 可选, 关闭 drawer 前调用,返回 boolean 类型,返回 false 可以阻止关闭 drawer 层 | [基本用法](demo#basic-usage) | | clickDoms | `array` | [] | 可选,isCover 为 false 的情况下,点击 Dom 关闭侧滑栏 | [关闭后不销毁](demo#do-not-destroy-after-closing) | | destroyOnHide | `boolean` | true | 可选,关闭 drawer 时是否销毁 DrawerComponent,默认销毁 | [关闭后不销毁](demo#do-not-destroy-after-closing) | diff --git a/devui/drawer/doc/api-en.md b/devui/drawer/doc/api-en.md index 2b2bb8c2..3214fdcb 100644 --- a/devui/drawer/doc/api-en.md +++ b/devui/drawer/doc/api-en.md @@ -41,7 +41,8 @@ Note: Components passed to drawerContentComponent in the API need to be register | Attribute | Type | Default | Description | Jump to Demo |Global Config| | :----------------: | :----------------------: | :------------------------: | :------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------: | -------------------------------------------------------------------- | -| drawerContentComponent | `Type` | -- | Required. The user-defined component is transferred. | [Basic Usage](demo#basic-usage) | +| drawerContentComponent | `Type` | -- | Optional. The user-defined component is transferred. | [Basic Usage](demo#basic-usage) | +| contentTemplate | `TemplateRef` | -- | Optional. Customized template, incompatible with drawerContentComponent. | [Custom Template](demo#template) | | componentFactoryResolver | `ComponentFactoryResolver` | Built-in | Optional. Generally this parameter is not required. | | injector | `Injector` | -- | Optional. You do not need to set this parameter. | | width | `string` | '300px' | Optional. Sets the width of the drawer. | [Basic Usage](demo#basic-usage) | diff --git a/devui/drawer/drawer.component.html b/devui/drawer/drawer.component.html index dd532d2f..f0fefae3 100755 --- a/devui/drawer/drawer.component.html +++ b/devui/drawer/drawer.component.html @@ -31,6 +31,9 @@
+
+ +
diff --git a/devui/drawer/drawer.component.ts b/devui/drawer/drawer.component.ts index 28e2d450..f223d0bc 100755 --- a/devui/drawer/drawer.component.ts +++ b/devui/drawer/drawer.component.ts @@ -1,7 +1,8 @@ import { AnimationEvent } from '@angular/animations'; +import { DOCUMENT } from '@angular/common'; import { - Component, Directive, ElementRef, HostListener, Input, OnDestroy, - OnInit, Renderer2, ViewChild, ViewContainerRef + Component, Directive, ElementRef, HostListener, Inject, Input, OnDestroy, + OnInit, Renderer2, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; import { backdropFadeInOut, flyInOut } from 'ng-devui/utils'; import { isNumber, parseInt, trim } from 'lodash-es'; @@ -59,8 +60,12 @@ export class DrawerComponent implements OnInit, OnDestroy { documentOverFlow: boolean; scrollTop: number; scrollLeft: number; + document: Document; - constructor(private elementRef: ElementRef, private renderer: Renderer2) { + contentTemplate: TemplateRef; + + constructor(private elementRef: ElementRef, private renderer: Renderer2, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit() { @@ -75,6 +80,9 @@ export class DrawerComponent implements OnInit, OnDestroy { }); } setWidth(width: string) { + if (typeof window === 'undefined') { + return; + } if (width.indexOf('%') >= 0) { const widthStr = trim(width, '%'); const widthNum = parseInt(widthStr, 10); @@ -116,19 +124,19 @@ export class DrawerComponent implements OnInit, OnDestroy { } show() { - if (document.documentElement.scrollHeight > document.documentElement.clientHeight) { + if (this.document.documentElement.scrollHeight > this.document.documentElement.clientHeight) { this.documentOverFlow = true; - this.scrollTop = document.documentElement.scrollTop || document.body.scrollTop; - this.scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; - this.renderer.addClass(document.body, 'devui-body-scrollblock'); - this.renderer.setStyle(document.body, 'top', `-${this.scrollTop}px`); - this.renderer.setStyle(document.body, 'left', `-${this.scrollLeft}px`); + this.scrollTop = this.document.documentElement.scrollTop || this.document.body.scrollTop; + this.scrollLeft = this.document.documentElement.scrollLeft || this.document.body.scrollLeft; + this.renderer.addClass(this.document.body, 'devui-body-scrollblock'); + this.renderer.setStyle(this.document.body, 'top', `-${this.scrollTop}px`); + this.renderer.setStyle(this.document.body, 'left', `-${this.scrollLeft}px`); } if (!this.bodyScrollable && this.documentOverFlow) { - this.renderer.addClass(document.body, 'devui-body-overflow-hidden'); + this.renderer.addClass(this.document.body, 'devui-body-overflow-hidden'); } this.animateState = 'in'; - const activeElement = document.activeElement; + const activeElement = this.document.activeElement; if (activeElement && typeof (activeElement['blur']) === 'function') { activeElement['blur'](); } @@ -149,7 +157,7 @@ export class DrawerComponent implements OnInit, OnDestroy { } else { const target: any = event.target; // 一定要document.contains(event.target),因为event.target可能已经不在document里了,这个时候就不能进hide了 - if (this.animateState === 'in' && (!this.elementRef.nativeElement.contains(target) && document.body.contains(target)) + if (this.animateState === 'in' && (!this.elementRef.nativeElement.contains(target) && this.document.body.contains(target)) && !this.isHaveDialogOrUpload()) { this.hide(); } @@ -173,14 +181,14 @@ export class DrawerComponent implements OnInit, OnDestroy { private hideOperation() { if (this.documentOverFlow) { - this.renderer.removeStyle(document.body, 'top'); - this.renderer.removeStyle(document.body, 'left'); - this.renderer.removeClass(document.body, 'devui-body-scrollblock'); - this.renderer.removeClass(document.body, 'devui-body-overflow-hidden'); - document.documentElement.scrollTop = this.scrollTop; - document.body.scrollTop = this.scrollTop; - document.documentElement.scrollLeft = this.scrollLeft; - document.body.scrollLeft = this.scrollLeft; + this.renderer.removeStyle(this.document.body, 'top'); + this.renderer.removeStyle(this.document.body, 'left'); + this.renderer.removeClass(this.document.body, 'devui-body-scrollblock'); + this.renderer.removeClass(this.document.body, 'devui-body-overflow-hidden'); + this.document.documentElement.scrollTop = this.scrollTop; + this.document.body.scrollTop = this.scrollTop; + this.document.documentElement.scrollLeft = this.scrollLeft; + this.document.body.scrollLeft = this.scrollLeft; } this.animateState = 'void'; if (this.subscription) { @@ -193,8 +201,8 @@ export class DrawerComponent implements OnInit, OnDestroy { destroy() { } isHaveDialogOrUpload() { - const dialog: any = document.getElementsByClassName('modal-dialog'); - const upload: any = document.getElementById('d-upload-temp'); + const dialog: any = this.document.getElementsByClassName('modal-dialog'); + const upload: any = this.document.getElementById('d-upload-temp'); return (dialog && dialog.length > 0) || upload; } @@ -218,6 +226,9 @@ export class DrawerComponent implements OnInit, OnDestroy { } private _setFullScreen(fullScreen?: boolean) { + if (typeof window === 'undefined') { + return; + } const drawerContainerEle = this.drawerContainer.nativeElement; if (this._width === this.oldWidth) { if (fullScreen === true || fullScreen === undefined) { diff --git a/devui/drawer/drawer.service.ts b/devui/drawer/drawer.service.ts index c7ada0d2..39346b88 100755 --- a/devui/drawer/drawer.service.ts +++ b/devui/drawer/drawer.service.ts @@ -30,7 +30,8 @@ export class DrawerService { destroyOnHide = true, position = 'right', bodyScrollable = true, - showAnimation = true + showAnimation = true, + contentTemplate }: IDrawerOptions): IDrawerOpenResult { const componentFactoryResolver_ = componentFactoryResolver || this.componentFactoryResolver; const drawerRef = this.overlayContainerRef.createComponent( @@ -50,15 +51,19 @@ export class DrawerService { position, backdropCloseable: isUndefined(backdropCloseable) ? true : backdropCloseable, bodyScrollable, - showAnimation + showAnimation, + contentTemplate }); - const drawerContentRef = drawerRef.instance.drawerContentHost.viewContainerRef.createComponent( - componentFactoryResolver_.resolveComponentFactory(drawerContentComponent), - 0, - injector - ); - assign(drawerContentRef.instance, data); + let drawerContentRef; + if (drawerContentComponent) { + drawerContentRef = drawerRef.instance.drawerContentHost.viewContainerRef.createComponent( + componentFactoryResolver_.resolveComponentFactory(drawerContentComponent), + 0, + injector + ); + assign(drawerContentRef.instance, data); + } drawerRef.instance.onHidden = () => { if (onClose) { @@ -80,7 +85,7 @@ export class DrawerService { drawerRef.instance.show(); return { drawerInstance: drawerRef.instance, - drawerContentInstance: drawerContentRef.instance + drawerContentInstance: drawerContentRef ? drawerContentRef.instance : null }; } } diff --git a/devui/drawer/drawer.types.ts b/devui/drawer/drawer.types.ts index 5ed50067..08d46a60 100755 --- a/devui/drawer/drawer.types.ts +++ b/devui/drawer/drawer.types.ts @@ -1,13 +1,14 @@ import { ComponentFactoryResolver, Injector, + TemplateRef, Type } from '@angular/core'; import { Observable } from 'rxjs'; import { DrawerComponent } from './drawer.component'; export interface IDrawerOptions { - drawerContentComponent: Type; + drawerContentComponent?: Type; componentFactoryResolver?: ComponentFactoryResolver; injector?: Injector; id?: string; @@ -26,6 +27,7 @@ export interface IDrawerOptions { beforeHidden?: () => boolean | Promise | Observable; bodyScrollable?: boolean; showAnimation?: boolean; + contentTemplate?: TemplateRef; } export interface IDrawerOpenResult { diff --git a/devui/dropdown/dropdown-menu.directive.ts b/devui/dropdown/dropdown-menu.directive.ts index 33f7af75..826721fd 100755 --- a/devui/dropdown/dropdown-menu.directive.ts +++ b/devui/dropdown/dropdown-menu.directive.ts @@ -1,5 +1,6 @@ import { animate, AnimationBuilder, AnimationMetadata, AnimationPlayer, style } from '@angular/animations'; -import { Directive, ElementRef, Host, HostBinding, HostListener, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Directive, ElementRef, Host, HostBinding, HostListener, Inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { AnimationCurves, AnimationDuration } from 'ng-devui/utils'; import { WindowRef } from 'ng-devui/window-ref'; import { fromEvent, Subscription } from 'rxjs'; @@ -16,16 +17,16 @@ export class DropDownMenuDirective implements OnInit, OnDestroy { @HostBinding('attr.tabIndex') tabIndex = -1; @HostBinding('class.devui-dropdown-menu') addClass = true; subscription: Subscription; - keydownEscapeEvent$ = fromEvent(document.body, 'keydown').pipe( - // chrome 为 Escape , ie 11为Esc - filter(event => (event).key === 'Escape' || (event).key === 'Esc') - ); + keydownEscapeEvent$; keydownEscapeSub: Subscription; popDirectionCache: 'top' | 'bottom'; private currentValue: any = false; constructor(@Host() private dropdown: DropDownDirective, private el: ElementRef, private render: Renderer2, - private windowRef: WindowRef, private builder: AnimationBuilder) { - + private windowRef: WindowRef, private builder: AnimationBuilder, @Inject(DOCUMENT) private doc: any) { + this.keydownEscapeEvent$ = fromEvent(this.doc.body, 'keydown').pipe( + // chrome 为 Escape , ie 11为Esc + filter(event => (event).key === 'Escape' || (event).key === 'Esc') + ); } ngOnInit() { diff --git a/devui/dropdown/dropdown.directive.ts b/devui/dropdown/dropdown.directive.ts index 27a14690..0fca4357 100755 --- a/devui/dropdown/dropdown.directive.ts +++ b/devui/dropdown/dropdown.directive.ts @@ -1,6 +1,7 @@ import { CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { DOCUMENT } from '@angular/common'; import { - AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, EventEmitter, HostBinding, Input, + AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, EventEmitter, HostBinding, Inject, Input, OnChanges, OnDestroy, Optional, Output, QueryList, SimpleChanges, SkipSelf } from '@angular/core'; @@ -78,6 +79,7 @@ export class DropDownDirective implements OnDestroy, OnChanges, AfterContentInit public cdkConnectedOverlayOrigin: CdkOverlayOrigin; private _appendToBody: boolean; + document: Document; public set appendToBody(bool: boolean) { this._appendToBody = (bool === true); @@ -103,8 +105,11 @@ export class DropDownDirective implements OnDestroy, OnChanges, AfterContentInit private cdr: ChangeDetectorRef, public el: ElementRef, private devConfigService: DevConfigService, - @Optional() @SkipSelf() public parentDropdown: DropDownDirective - ) { } + @Optional() @SkipSelf() public parentDropdown: DropDownDirective, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } ngOnChanges(changes: SimpleChanges) { if (changes.hasOwnProperty('trigger')) { @@ -201,7 +206,7 @@ export class DropDownDirective implements OnDestroy, OnChanges, AfterContentInit } simulateEventDispatch($event, target?) { - const event = document.createEvent('MouseEvents'); + const event = this.document.createEvent('MouseEvents'); event.initEvent($event.type, true, true); event['originEvent'] = $event['originEvent'] || $event; diff --git a/devui/dropdown/dropdown.service.ts b/devui/dropdown/dropdown.service.ts index 203300a8..ae8f46f6 100755 --- a/devui/dropdown/dropdown.service.ts +++ b/devui/dropdown/dropdown.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; import { DropDownDirective } from './dropdown.directive'; @Injectable() @@ -6,12 +7,16 @@ export class DropDownService { private openScope: DropDownDirective; private closeDropdownBind: EventListener = this.closeDropdown.bind(this); + document: Document; + constructor(@Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } public open(dropdownScope: DropDownDirective) { if (!this.openScope) { // 延时绑定document事件,防止事件冒泡导致立即触发 setTimeout(() => { - window.document.addEventListener('click', this.closeDropdownBind); + this.document.addEventListener('click', this.closeDropdownBind); }); } this.openScope = dropdownScope; @@ -22,7 +27,7 @@ export class DropDownService { return; } this.openScope = null; - window.document.removeEventListener('click', this.closeDropdownBind); + this.document.removeEventListener('click', this.closeDropdownBind); } private closeDropdown(event: MouseEvent) { diff --git a/devui/form/services/d-validate-sync.service.ts b/devui/form/services/d-validate-sync.service.ts index f64de7d9..fd9d8738 100644 --- a/devui/form/services/d-validate-sync.service.ts +++ b/devui/form/services/d-validate-sync.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@angular/core'; import { AbstractControl } from '@angular/forms'; import { Subscription } from 'rxjs'; -const resolvedPromise = (() => Promise.resolve(null))(); +function resolvedPromiseFunc() { return Promise.resolve(null); } +const resolvedPromise = resolvedPromiseFunc(); @Injectable({ providedIn: 'root' }) export class DValidateSyncService { diff --git a/devui/fullscreen/fullscreen.component.ts b/devui/fullscreen/fullscreen.component.ts index 54f338c8..d5a55c4d 100644 --- a/devui/fullscreen/fullscreen.component.ts +++ b/devui/fullscreen/fullscreen.component.ts @@ -18,17 +18,20 @@ export class FullscreenComponent implements OnInit, OnDestroy, AfterViewInit { @Input() target: HTMLElement; @Output() fullscreenLaunch: EventEmitter = new EventEmitter(); + document: Document; constructor( private elementRef: ElementRef, @Inject(DOCUMENT) private doc: any - ) { } + ) { + this.document = this.doc; + } ngOnInit() { - document.addEventListener('fullscreenchange', this.onFullScreenChange); - document.addEventListener('MSFullscreenChange', this.onFullScreenChange); - document.addEventListener('webkitfullscreenchange', this.onFullScreenChange); - document.addEventListener('keydown', this.handleKeyDown); + this.document.addEventListener('fullscreenchange', this.onFullScreenChange); + this.document.addEventListener('MSFullscreenChange', this.onFullScreenChange); + this.document.addEventListener('webkitfullscreenchange', this.onFullScreenChange); + this.document.addEventListener('keydown', this.handleKeyDown); } ngAfterViewInit() { @@ -144,19 +147,19 @@ export class FullscreenComponent implements OnInit, OnDestroy, AfterViewInit { } ngOnDestroy() { - document.removeEventListener('fullscreenchange', this.onFullScreenChange); - document.removeEventListener('MSFullscreenChange', this.onFullScreenChange); - document.removeEventListener('webkitfullscreenchange', this.onFullScreenChange); - document.removeEventListener('keydown', this.handleKeyDown); + this.document.removeEventListener('fullscreenchange', this.onFullScreenChange); + this.document.removeEventListener('MSFullscreenChange', this.onFullScreenChange); + this.document.removeEventListener('webkitfullscreenchange', this.onFullScreenChange); + this.document.removeEventListener('keydown', this.handleKeyDown); const btnLaunch = this.elementRef.nativeElement.querySelector('[fullscreen-launch]'); if (btnLaunch) { btnLaunch.removeEventListener('click', this.handleFullscreen); } } private addFullScreenStyle() { - document.getElementsByTagName('html')[0].classList.add('devui-fullscreen'); + this.document.getElementsByTagName('html')[0].classList.add('devui-fullscreen'); } private removeFullScreenStyle() { - document.getElementsByTagName('html')[0].classList.remove('devui-fullscreen'); + this.document.getElementsByTagName('html')[0].classList.remove('devui-fullscreen'); } } diff --git a/devui/gantt/demo/basic/basic.component.html b/devui/gantt/demo/basic/basic.component.html index 82acb93d..0e103512 100644 --- a/devui/gantt/demo/basic/basic.component.html +++ b/devui/gantt/demo/basic/basic.component.html @@ -1,7 +1,7 @@
- +
diff --git a/devui/gantt/demo/basic/basic.component.ts b/devui/gantt/demo/basic/basic.component.ts index db44ebd1..28a51889 100644 --- a/devui/gantt/demo/basic/basic.component.ts +++ b/devui/gantt/demo/basic/basic.component.ts @@ -54,7 +54,7 @@ export class BasicComponent implements OnInit, AfterViewInit, OnDestroy { goToday() { const today = new Date(); - const offset = this.ganttService.getDatePostionOffset(today) - this.scrollElement.clientWidth / 2; + const offset = this.ganttService.getDatePostionOffset(today) - this.ganttService.getScaleUnitPixel() * 3; if (this.scrollElement) { this.scrollElement.scrollTo(offset, this.scrollElement.scrollTop); } @@ -105,8 +105,11 @@ export class BasicComponent implements OnInit, AfterViewInit, OnDestroy { this.mouseDownHandler = this.ganttService.mouseDownListener.subscribe(this.onMousedown.bind(this)); this.mouseMoveHandler = this.ganttService.mouseMoveListener.subscribe(this.onMouseMove.bind(this)); this.mouseEndHandler = this.ganttService.mouseEndListener.subscribe(this.onMouseEnd.bind(this)); + this.goToday(); } + onGanttBarMoveEnd (e) {} + onMousedown(pageX) { this.startMove = true; this.originOffsetLeft = this.scrollElement.scrollLeft; diff --git a/devui/gantt/demo/table/table.component.html b/devui/gantt/demo/table/table.component.html index e8276a17..7bf06760 100644 --- a/devui/gantt/demo/table/table.component.html +++ b/devui/gantt/demo/table/table.component.html @@ -43,9 +43,9 @@ > status - + = treeDataSource; @ViewChild('datatable', { read: ElementRef, static: true }) datatableElementRef: ElementRef; - @ViewChild('ganttscale', { read: ElementRef, static: true }) ganttscaleElementRef: ElementRef; ganttScaleWidth: string; ganttBarContainerElement: Element; resizeHandleContainerElement: Element; @@ -107,6 +106,7 @@ export class TableComponent implements OnInit, AfterViewInit, OnDestroy { this.mouseDownHandler = this.ganttService.mouseDownListener.subscribe(this.onMousedown.bind(this)); this.mouseMoveHandler = this.ganttService.mouseMoveListener.subscribe(this.onMouseMove.bind(this)); this.mouseEndHandler = this.ganttService.mouseEndListener.subscribe(this.onMouseEnd.bind(this)); + this.goToday(); }); } @@ -185,10 +185,6 @@ export class TableComponent implements OnInit, AfterViewInit, OnDestroy { this.originOffsetLeft = this.tableScrollLeft; } - onGanttBarMoving(info: GanttTaskInfo) { - this.adjustScrollView(info); - } - onGanttBarResizeStart() { this.originOffsetLeft = this.tableScrollLeft; } @@ -198,12 +194,7 @@ export class TableComponent implements OnInit, AfterViewInit, OnDestroy { } adjustScrollView(info: GanttTaskInfo) { - if (info.left + info.width > this.scrollView.scrollLeft + this.scrollView.clientWidth - this.originOffsetX) { - this.scrollView.scrollTo(this.scrollView.scrollLeft + this.scaleStep, this.scrollView.scrollTop); - } - if (info.left < this.scrollView.scrollLeft) { - this.scrollView.scrollTo(this.scrollView.scrollLeft - this.scaleStep, this.scrollView.scrollTop); - } + console.log(info); } onGanttBarMove(info: GanttTaskInfo) { diff --git a/devui/gantt/gantt-bar/gantt-bar.component.ts b/devui/gantt/gantt-bar/gantt-bar.component.ts index c8ed2d94..184394e0 100644 --- a/devui/gantt/gantt-bar/gantt-bar.component.ts +++ b/devui/gantt/gantt-bar/gantt-bar.component.ts @@ -82,6 +82,13 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe public tipHovered = false; public mouseMoveOnBar = false; percentage: number; + private movedOut = false; + private mouseenterHandler: Subscription; + private mouseleaveHandler: Subscription; + private scrollTimer = null; + private SCROLL_STEP = 10; + private outDirection = 'right'; + private scrollViewRange = [0, 0]; @Input() barMoveDisabled = false; @Input() barResizeDisabled = false; @@ -107,6 +114,8 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe @Input() customTitleClass = ''; + @Input() scrollElement: HTMLElement; + @Output() barMoveStartEvent = new EventEmitter(); @Output() barMovingEvent = new EventEmitter(); @Output() barMoveEndEvent = new EventEmitter(); @@ -340,6 +349,10 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe } private barStartMoving(value: number): void { + this.scrollViewRange = [ + this.scrollElement.getBoundingClientRect().left + this.originOffsetX, + this.scrollElement.getBoundingClientRect().right, + ]; this.moveBarStart = true; this.barMoveStartPageX = value; this.barOriginLeft = parseInt(this.ganttBar.nativeElement.style.left, 10); @@ -431,6 +444,10 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe } if (this.moveBarStart) { + this.checkIsOut(value); + if (this.movedOut) { + return; + } this.mouseMoveOnBar = true; const offset = value - this.barMoveStartPageX; @@ -446,16 +463,11 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe const earlyDateTime = this.startDate.getTime() - this.EARLYOFFSET * GanttService.DAY_DURATION; const lateDateTime = this.endDate.getTime() + this.EARLYOFFSET * GanttService.DAY_DURATION; - - if (offset < 0 && earlyDateTime < this.ganttService.scaleStartDate.getTime()) { + if (earlyDateTime < this.ganttService.scaleStartDate.getTime()) { this.ganttService.setScaleConfig({ startDate: new Date(earlyDateTime) }); - this.barOriginLeft = this.EARLYOFFSET * this.ganttService.getScaleUnitPixel() - Math.round(offset); - } - - if (offset > 0 && lateDateTime > this.ganttService.scaleEndDate.getTime()) { + } else if (lateDateTime > this.ganttService.scaleEndDate.getTime()) { this.ganttService.setScaleConfig({ endDate: new Date(lateDateTime) }); } - const finalLeft = this.barOriginLeft + Math.round(offset); this.left = finalLeft; @@ -466,6 +478,59 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe } } + private checkIsOut(value) { + this.outDirection = value - this.barMoveStartPageX > 0 ? 'right' : 'left'; + if (this.outDirection === 'left') { + this.movedOut = value < this.scrollViewRange[0]; + } else { + this.movedOut = value > this.scrollViewRange[1]; + } + if (this.movedOut) { + this.autoScroll(); + } else if (this.scrollTimer) { + const left = + this.outDirection === 'left' + ? this.scrollElement.scrollLeft + (value - this.scrollElement.getBoundingClientRect().left) - this.originOffsetX + : this.scrollElement.scrollLeft + this.scrollElement.clientWidth - this.originOffsetX; + this.setLeft(Math.round(left)); + this.stopAutoScroll(); + this.barMoveStartPageX = value; + } + } + + private autoScroll() { + if (!this.scrollTimer) { + this.scrollTimer = setInterval(() => { + this.outDirection === 'left' + ? (this.scrollElement.scrollLeft -= this.SCROLL_STEP) + : (this.scrollElement.scrollLeft += this.SCROLL_STEP); + }, 10); + } + } + + private stopAutoScroll() { + clearInterval(this.scrollTimer); + this.scrollTimer = null; + } + + private setLeft(left) { + const offset = left - this.left; + const timeOffset = (Math.round(offset) / this.ganttService.getScaleUnitPixel()) * GanttService.DAY_DURATION; + + const newStartDate = new Date(this.startDate.getTime() + timeOffset); + this.ganttService.roundDate(newStartDate); + this.startDate = newStartDate; + + const newEndDate = new Date(this.endDate.getTime() + timeOffset); + this.ganttService.roundDate(newEndDate); + this.endDate = newEndDate; + + this.originStartDate = this.startDate; + this.originEndDate = this.endDate; + this.barOriginLeft = left; + this.left = left; + } + private getGanttTaskInfo(): GanttTaskInfo { this.duration = this.ganttService.getDuration(this.startDate, this.endDate) + 'd'; const progress = this.progressRate + '%'; @@ -508,6 +573,7 @@ export class GanttBarComponent implements OnInit, OnChanges, AfterViewInit, OnDe this.barProgressEvent.emit(this.progressRate); } this.handleController(false); + this.stopAutoScroll(); this.cdr.markForCheck(); } diff --git a/devui/gantt/gantt-scale/gantt-scale.component.html b/devui/gantt/gantt-scale/gantt-scale.component.html index 9bac685c..e493d72b 100644 --- a/devui/gantt/gantt-scale/gantt-scale.component.html +++ b/devui/gantt/gantt-scale/gantt-scale.component.html @@ -1,7 +1,9 @@
(); + private scrollHandler: Subscription; + i18nText: I18nInterface['gantt']; i18nLocale: I18nInterface['locale']; i18nCommonText: I18nInterface['common']; @@ -56,24 +74,52 @@ export class GanttScaleComponent implements OnInit, OnChanges, OnDestroy { } if (config.startDate || config.endDate) { this.scaleData = this.generateScaleData(this.startDate, this.endDate); + this.getViewScaleData(); } if (config.unit) { this.unit = config.unit; + this.getViewScaleData(); } }); } ngOnChanges(changes: SimpleChanges) { + if (changes.hasOwnProperty('scrollElement')) { + this.registerScrollEvent(); + } + } + + registerScrollEvent () { + if (!this.scrollHandler && this.scrollElement) { + this.scrollHandler = fromEvent(this.scrollElement, 'scroll').subscribe(e => { + this.getViewScaleData(); + }); + } + } + + getViewScaleData() { + if (this.scrollElement) { + const containerWidth = this.scrollElement.clientWidth; + const scrollLeft = this.scrollElement.scrollLeft; + const start = Math.floor(scrollLeft / this.scaleWidth[this.unit]); + const offset = Math.ceil(containerWidth / this.scaleWidth[this.unit]); + this.viewScaleRange = [start - 2, start + offset + 2]; + this.viewSCaleData = this.scaleData.filter((i: GanttScaleDateInfo) => { + return i.index >= this.viewScaleRange[0] && i.index <= this.viewScaleRange[1]; + }); + } } private generateScaleData(startDate: Date, endDate: Date): GanttScaleDateInfo[] { if (startDate && endDate) { const scaleData = []; let handleDate = startDate; + let index = 0; while (!this.ganttService.isSomeDate(handleDate, endDate)) { - const dateInfo = this.generateDateInfo(handleDate); + const dateInfo = this.generateDateInfo(handleDate, index); scaleData.push(dateInfo); handleDate = this.getNextDay(new Date(handleDate)); + index++; } return scaleData; } @@ -84,7 +130,7 @@ export class GanttScaleComponent implements OnInit, OnChanges, OnDestroy { return new Date(nextDayDate); } - private generateDateInfo(date: Date): GanttScaleDateInfo { + private generateDateInfo(date: Date, index): GanttScaleDateInfo { const dateInfo = { dayOfMonthLabel: '', dayOfWeekLabel: '', @@ -96,7 +142,8 @@ export class GanttScaleComponent implements OnInit, OnChanges, OnDestroy { today: false, milestone: '', highlightStart: false, - scaleStartVisable: true + scaleStartVisable: true, + index }; const dayOfMonth = date.getDate(); @@ -166,8 +213,8 @@ export class GanttScaleComponent implements OnInit, OnChanges, OnDestroy { if (highlightData.length === 1) { const startData = highlightData[0]; - this.highlightStartText = this.prefixZero(parseInt(startData.monthLabel, 10)) + '-' + - this.prefixZero(parseInt(startData.dayOfMonthLabel, 10)); + this.highlightStartText = + this.prefixZero(parseInt(startData.monthLabel, 10)) + '-' + this.prefixZero(parseInt(startData.dayOfMonthLabel, 10)); const highlightWidth = this.ganttService.getScaleUnitPixel(); if (highlightWidth < 40) { this.highlightMinWidth = 40; @@ -177,10 +224,10 @@ export class GanttScaleComponent implements OnInit, OnChanges, OnDestroy { } else { const startData = highlightData[0]; const endData = highlightData[highlightData.length - 1]; - this.highlightStartText = this.prefixZero(parseInt(startData.monthLabel, 10)) + '-' + - this.prefixZero(parseInt(startData.dayOfMonthLabel, 10)); - this.highlightEndText = this.prefixZero(parseInt(endData.monthLabel, 10)) + '-' + - this.prefixZero(parseInt(endData.dayOfMonthLabel, 10)); + this.highlightStartText = + this.prefixZero(parseInt(startData.monthLabel, 10)) + '-' + this.prefixZero(parseInt(startData.dayOfMonthLabel, 10)); + this.highlightEndText = + this.prefixZero(parseInt(endData.monthLabel, 10)) + '-' + this.prefixZero(parseInt(endData.dayOfMonthLabel, 10)); const highlightWidth = highlightData.length * this.ganttService.getScaleUnitPixel(); if (highlightWidth < 80) { this.highlightMinWidth = 80; @@ -208,5 +255,8 @@ export class GanttScaleComponent implements OnInit, OnChanges, OnDestroy { this.ganttSacleConfigHandler.unsubscribe(); this.ganttSacleConfigHandler = null; } + if (this.scrollHandler) { + this.scrollHandler.unsubscribe(); + } } } diff --git a/devui/gantt/gantt-tools/gantt-tools.component.html b/devui/gantt/gantt-tools/gantt-tools.component.html index c65c8318..476074c5 100644 --- a/devui/gantt/gantt-tools/gantt-tools.component.html +++ b/devui/gantt/gantt-tools/gantt-tools.component.html @@ -1,8 +1,8 @@
- {{ 'today' }} + {{ 'Today' }}
- - {{ currentUnit }} + + {{ currentUnitLabel }}
- + - + diff --git a/devui/gantt/gantt-tools/gantt-tools.component.scss b/devui/gantt/gantt-tools/gantt-tools.component.scss index 79163296..2b75ce01 100644 --- a/devui/gantt/gantt-tools/gantt-tools.component.scss +++ b/devui/gantt/gantt-tools/gantt-tools.component.scss @@ -20,6 +20,10 @@ box-shadow: $devui-shadow-length-base rgba(81, 112, 255, 0.4); cursor: pointer; + &.disabled { + opacity: 0.5; + } + span { border: 0 !important; } @@ -33,6 +37,11 @@ } } +.devui-dropdown-menu { + top: 10px !important; + left: -6px !important; +} + :host ::ng-deep { .devui-dropdown-origin { border: 0; diff --git a/devui/gantt/gantt-tools/gantt-tools.component.ts b/devui/gantt/gantt-tools/gantt-tools.component.ts index 5bf4120d..f5bcf33b 100644 --- a/devui/gantt/gantt-tools/gantt-tools.component.ts +++ b/devui/gantt/gantt-tools/gantt-tools.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { GanttScaleUnit, UnitRole } from '../gantt.model'; +import { UnitRole } from '../gantt.model'; @Component({ selector: 'd-gantt-tools', @@ -8,7 +8,15 @@ import { GanttScaleUnit, UnitRole } from '../gantt.model'; }) export class GanttToolsComponent { - @Input() currentUnit: GanttScaleUnit; + @Input('currentUnit') set currentUnit(val) { + this._currentUnit = val; + const data = this.views.filter(i => i.value === val); + this.currentUnitLabel = data.length > 0 ? data[0].label : ''; + } + + get currentUnit () { + return this._currentUnit; + } @Input() isFullScreen = false; @@ -22,17 +30,21 @@ export class GanttToolsComponent { unitRole = UnitRole; + currentUnitLabel = ''; + + _currentUnit = ''; + views = [ { - label: 'day', + label: 'Day', value: 'day' }, { - label: 'week', + label: 'Week', value: 'week' }, { - label: 'month', + label: 'Month', value: 'month' } ]; @@ -52,7 +64,6 @@ export class GanttToolsComponent { } selectView (menu) { - this.currentUnit = menu.value; - this.switchView.emit(this.currentUnit); + this.switchView.emit(menu.value); } } diff --git a/devui/gantt/gantt.model.ts b/devui/gantt/gantt.model.ts index f7526109..7b525324 100644 --- a/devui/gantt/gantt.model.ts +++ b/devui/gantt/gantt.model.ts @@ -11,6 +11,7 @@ export interface GanttScaleDateInfo { highlightStart?: boolean; milestone?: string; scaleStartVisable?: boolean; + index?: number; } export enum GanttScaleUnit { diff --git a/devui/gantt/gantt.service.ts b/devui/gantt/gantt.service.ts index 47f35d16..a03cc900 100644 --- a/devui/gantt/gantt.service.ts +++ b/devui/gantt/gantt.service.ts @@ -52,13 +52,13 @@ export class GanttService { getScaleUnitPixel() { switch (this.scaleUnit) { case GanttScaleUnit.day: - return 50; + return 40; break; case GanttScaleUnit.week: - return 20; + return 30; break; case GanttScaleUnit.month: - return 10; + return 20; break; default: break; diff --git a/devui/gantt/gantt.spec.ts b/devui/gantt/gantt.spec.ts index f0f616f7..ddc6977f 100644 --- a/devui/gantt/gantt.spec.ts +++ b/devui/gantt/gantt.spec.ts @@ -2,8 +2,8 @@ import { Component, DebugElement, ElementRef, OnDestroy, OnInit, ViewChild } fro import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { - GanttBarComponent, GanttModule, GanttScaleComponent, GanttScaleUnit, - GanttService, GanttTaskInfo + GanttBarComponent, GanttModule, GanttScaleComponent, GanttScaleUnit, + GanttService, GanttTaskInfo } from 'ng-devui/gantt'; import { I18nModule } from 'ng-devui/i18n'; import { Subscription } from 'rxjs'; @@ -22,6 +22,7 @@ import { DomHelper } from '../utils/testing/dom-helper'; [endDate]="item?.endDate" [tipTemplateRef]="tipTemplate" [id]="item?.id" + [scrollElement]="ganttContainer" [progressRate]="item?.progressRate" (barMoveStartEvent)="onGanttBarMoveStart($event)" (barMovingEvent)="onGanttBarMoving($event)" @@ -163,30 +164,24 @@ describe('gantt', () => { it('should init ok', () => { expect(component).toBeTruthy(); const header = debugEl.query(By.css('.header')).nativeElement; - expect(header.style.width).toBe('1600px'); + expect(header.style.width).toBe('1280px'); const body = debugEl.query(By.css('.body')).nativeElement; - expect(body.style.width).toBe('1600px'); + expect(body.style.width).toBe('1280px'); const ganttScale = debugEl.query(By.directive(GanttScaleComponent)).nativeElement; - expect(ganttScale.clientWidth).toBe(1600); + expect(ganttScale.clientWidth).toBe(1280); const scales = ganttScale.querySelectorAll('.devui-gantt-scale'); - expect(scales.length).toBe(31); + expect(scales.length).toBe(0); const ganttBar = debugEl.query(By.directive(GanttBarComponent)).nativeElement; const ganttBarElement = ganttBar.querySelector('.devui-gantt-bar'); - expect(ganttBarElement.style.width).toBe('300px'); - expect(ganttBarElement.style.left).toBe('200px'); + expect(ganttBarElement.style.width).toBe('240px'); + expect(ganttBarElement.style.left).toBe('160px'); const ganttBarTrack = ganttBar.querySelector('.devui-gantt-bar-track'); expect(ganttBarTrack.style.width).toBe('30%'); - const ganttMonthMark = debugEl.query(By.css('.devui-mark-line')).nativeElement; - expect(ganttMonthMark.style.left).toBe('0px'); - - const ganttMarkStripes = debugEl.queryAll(By.css('.devui-mark-stripe')); - expect(ganttMarkStripes.length).toBe(5); - expect(ganttMarkStripes[0].nativeElement.style.left).toBe('50px'); }); it('should mouse over&leave on bar ok', fakeAsync(() => { @@ -196,25 +191,9 @@ describe('gantt', () => { tick(250); fixture.detectChanges(); - let ganttTips = document.querySelector('.devui-gantt-tips'); + const ganttTips = document.querySelector('.devui-gantt-tips'); expect(ganttTips).toBeTruthy(); - const ganttScale = debugEl.query(By.directive(GanttScaleComponent)).nativeElement; - let highLight = ganttScale.querySelector('.scale-highlight'); - expect(highLight.clientWidth).toBe(300); - - const hightLightFirstChild = highLight.firstElementChild; - expect(hightLightFirstChild.innerText).toBe('05-05'); - expect(hightLightFirstChild.nextElementSibling.innerText).toBe('05-10'); - - ganttBarElement.dispatchEvent(new Event('mouseleave')); - tick(250); - fixture.detectChanges(); - ganttTips = document.querySelector('.devui-gantt-tips'); - expect(ganttTips).toBeNull(); - - highLight = ganttScale.querySelector('.scale-highlight'); - expect(highLight).toBeNull(); })); it('should drag progress ok', fakeAsync(() => { @@ -231,7 +210,7 @@ describe('gantt', () => { fixture.detectChanges(); const ganttBarTrack = ganttBar.querySelector('.devui-gantt-bar-track'); - expect(ganttBarTrack.style.width).toBe('70%'); + expect(ganttBarTrack.style.width).toBe('100%'); expect(component.currentAction).toBe('onBarProgressEvent'); })); @@ -250,14 +229,10 @@ describe('gantt', () => { fixture.detectChanges(); const ganttBarElement = ganttBar.querySelector('.devui-gantt-bar'); - expect(ganttBarElement.style.left).toBe('650px'); + expect(ganttBarElement.style.left).toBe('600px'); const ganttScale = debugEl.query(By.directive(GanttScaleComponent)).nativeElement; - const highLight = ganttScale.querySelector('.scale-highlight'); - const hightLightFirstChild = highLight.firstElementChild; - expect(hightLightFirstChild.innerText).toBe('05-14'); - expect(hightLightFirstChild.nextElementSibling.innerText).toBe('05-19'); - expect(Number(component.list[0].startDate)).toBe(1589385600000); + expect(Number(component.list[0].startDate)).toBe(1589558400000); })); it('should resize bar ok', fakeAsync(() => { @@ -272,36 +247,28 @@ describe('gantt', () => { fixture.detectChanges(); expect(component.currentAction).toBe('onGanttBarResizeStart'); - document.dispatchEvent(new MouseEvent('mousemove', { clientX: 50 })); + document.dispatchEvent(new MouseEvent('mousemove', { clientX: 40 })); fixture.detectChanges(); expect(component.currentAction).toBe('onGanttBarResizing'); document.dispatchEvent(new MouseEvent('mouseup')); fixture.detectChanges(); - expect(ganttBarElement.style.width).toBe('350px'); + expect(ganttBarElement.style.width).toBe('280px'); expect(Number(component.list[0].endDate)).toBe(1589126400000); draggerLeft.dispatchEvent(new MouseEvent('mousedown')); fixture.detectChanges(); expect(component.currentAction).toBe('onGanttBarResizeStart'); - document.dispatchEvent(new MouseEvent('mousemove', { clientX: -50 })); + document.dispatchEvent(new MouseEvent('mousemove', { clientX: -40 })); fixture.detectChanges(); expect(component.currentAction).toBe('onGanttBarResizing'); document.dispatchEvent(new MouseEvent('mouseup')); fixture.detectChanges(); - expect(ganttBarElement.style.left).toBe('150px'); - expect(ganttBarElement.style.width).toBe('400px'); + expect(ganttBarElement.style.left).toBe('120px'); + expect(ganttBarElement.style.width).toBe('320px'); expect(Number(component.list[0].startDate)).toBe(1588521600000); - - const ganttScale = debugEl.query(By.directive(GanttScaleComponent)).nativeElement; - const highLight = ganttScale.querySelector('.scale-highlight'); - expect(highLight.clientWidth).toBe(400); - - const hightLightFirstChild = highLight.firstElementChild; - expect(hightLightFirstChild.innerText).toBe('05-04'); - expect(hightLightFirstChild.nextElementSibling.innerText).toBe('05-11'); })); }); }); diff --git a/devui/gantt/public-api.ts b/devui/gantt/public-api.ts index 2a1f9c87..c2f25589 100644 --- a/devui/gantt/public-api.ts +++ b/devui/gantt/public-api.ts @@ -1,9 +1,10 @@ -export * from './gantt.module'; -export * from './gantt-scale/gantt-scale.component'; -export * from './gantt-bar/gantt-bar.component'; export * from './gantt-bar-parent/gantt-bar-parent.component'; +export * from './gantt-bar/gantt-bar.component'; +export * from './gantt-marker.directive'; export * from './gantt-milestone/gantt-milestone.component'; -export * from './resize-handle.directive'; -export * from './gantt.service'; +export * from './gantt-scale/gantt-scale.component'; +export * from './gantt-tools/gantt-tools.component'; export * from './gantt.model'; -export * from './gantt-marker.directive'; +export * from './gantt.module'; +export * from './gantt.service'; +export * from './resize-handle.directive'; diff --git a/devui/gantt/resize-handle.directive.ts b/devui/gantt/resize-handle.directive.ts index 9ed27f94..47f073de 100644 --- a/devui/gantt/resize-handle.directive.ts +++ b/devui/gantt/resize-handle.directive.ts @@ -1,4 +1,5 @@ -import { Directive, ElementRef, EventEmitter, HostListener, Input, NgZone, Output, Renderer2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Directive, ElementRef, EventEmitter, HostListener, Inject, Input, NgZone, Output, Renderer2 } from '@angular/core'; import { fromEvent, Subscription } from 'rxjs'; @Directive({ @@ -27,8 +28,11 @@ export class ResizeHandleDirective { resizeHandleLeave: any; resizeHandleClick: any; preventRemoveHandle = false; - constructor(element: ElementRef, private renderer2: Renderer2, private zone: NgZone) { + document: Document; + + constructor(element: ElementRef, private renderer2: Renderer2, private zone: NgZone, @Inject(DOCUMENT) private doc: any) { this.element = element.nativeElement; + this.document = this.doc; } @HostListener('mouseenter', ['$event']) @@ -37,8 +41,8 @@ export class ResizeHandleDirective { this.resizeHandle = this.renderer2.createElement('div'); this.renderer2.appendChild(this.containerElement, this.resizeHandle); this.renderer2.addClass(this.resizeHandle, 'resize-handle'); - const left = this.element.getBoundingClientRect().left - this.containerElement.getBoundingClientRect().left; - this.renderer2.setStyle(this.resizeHandle, 'left', left + this.element.clientWidth + 'px'); + const left = this.element.getBoundingClientRect().right - this.containerElement.getBoundingClientRect().left; + this.renderer2.setStyle(this.resizeHandle, 'left', left + 'px'); this.resizeHandleEnter = this.renderer2.listen(this.resizeHandle, 'mouseenter', this.onHandleMouseEnter.bind(this)); this.resizeHandleLeave = this.renderer2.listen(this.resizeHandle, 'mouseleave', this.onHandleMouseLeave.bind(this)); this.resizeHandleClick = this.renderer2.listen(this.resizeHandle, 'mousedown', this.onMousedown.bind(this)); @@ -104,7 +108,7 @@ export class ResizeHandleDirective { this.mouseUpSubscription = mouseup.subscribe((ev: MouseEvent) => this.onMouseup(ev)); this.zone.runOutsideAngular(() => { - window.document.addEventListener('mousemove', this.bindMousemove); + this.document.addEventListener('mousemove', this.bindMousemove); }); } @@ -125,7 +129,7 @@ export class ResizeHandleDirective { this._destroySubscription(); } - window.document.removeEventListener('mousemove', this.bindMousemove); + this.document.removeEventListener('mousemove', this.bindMousemove); } bindMousemove = (e) => { diff --git a/devui/i18n/en-us.ts b/devui/i18n/en-us.ts index 4a7195fa..457bc067 100644 --- a/devui/i18n/en-us.ts +++ b/devui/i18n/en-us.ts @@ -251,7 +251,9 @@ export default { noFilterConditions: 'No filter conditions', clearFilterCondition: 'Clear Filter Condition', seeMore: 'See more', - selected: 'Selected' + selected: 'Selected', + switchToStart: 'Switch To Start', + switchToEnd: 'Switch To End' }, userGuide: { guide: 'Guide', diff --git a/devui/i18n/i18n.model.ts b/devui/i18n/i18n.model.ts index c497e589..7efc2d99 100644 --- a/devui/i18n/i18n.model.ts +++ b/devui/i18n/i18n.model.ts @@ -148,6 +148,9 @@ export interface I18nInterface { clearFilterCondition: string; seeMore: string; selected: string; + switchToStart: string; + switchToEnd: string; + }; userGuide: { guide: string; diff --git a/devui/i18n/zh-cn.ts b/devui/i18n/zh-cn.ts index 5236f572..13424895 100644 --- a/devui/i18n/zh-cn.ts +++ b/devui/i18n/zh-cn.ts @@ -251,7 +251,9 @@ export default { noFilterConditions: '没有筛选条件', clearFilterCondition: '清空', seeMore: '查看全部过滤条件', - selected: '已选择' + selected: '已选择', + switchToStart: '切换至开始时间', + switchToEnd: '切换至结束时间' }, userGuide: { guide: '指引', diff --git a/devui/image-preview/demo/z-index/z-index.component.html b/devui/image-preview/demo/z-index/z-index.component.html index 3bce25c5..f45af807 100644 --- a/devui/image-preview/demo/z-index/z-index.component.html +++ b/devui/image-preview/demo/z-index/z-index.component.html @@ -1,13 +1,13 @@
- 1000 + 1000 default: 1050 - 1100 + 1100 zIndex: {{ zIndex }}
- 900 + 900 default: 1040 - 1000 + 1000 backDropZIndex: {{ backDropZIndex }}
diff --git a/devui/image-preview/image-preview.component.ts b/devui/image-preview/image-preview.component.ts index bfece3f0..710d60c6 100644 --- a/devui/image-preview/image-preview.component.ts +++ b/devui/image-preview/image-preview.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnDestroy, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { I18nInterface, I18nService } from 'ng-devui/i18n'; import { Subscription } from 'rxjs'; import { TransformableElement } from './transformable-element'; @@ -37,6 +38,7 @@ export class DImagePreviewComponent implements OnInit, OnDestroy { i18nText: I18nInterface['imagePreview']; i18nSubscription: Subscription; + document: Document; get targetImageSrc(): string { // 防止targetImageIndex出现-1的情况 @@ -46,8 +48,11 @@ export class DImagePreviewComponent implements OnInit, OnDestroy { constructor( private elementRef: ElementRef, - private i18n: I18nService - ) { } + private i18n: I18nService, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } @HostListener('click', ['$event']) click($event) { @@ -124,11 +129,11 @@ export class DImagePreviewComponent implements OnInit, OnDestroy { } private addFullScreenStyle() { - document.querySelector('body').classList.add('devui-fullscreen'); + this.document.querySelector('body').classList.add('devui-fullscreen'); } private removeFullScreenStyle() { - document.querySelector('body').classList.remove('devui-fullscreen'); + this.document.querySelector('body').classList.remove('devui-fullscreen'); } private getImgElement(): HTMLElement { diff --git a/devui/image-preview/transformable-element.ts b/devui/image-preview/transformable-element.ts index 15c79866..abdd8753 100644 --- a/devui/image-preview/transformable-element.ts +++ b/devui/image-preview/transformable-element.ts @@ -40,6 +40,9 @@ export class TransformableElement { } setElementListener() { + if (typeof window === 'undefined') { + return; + } if (this.eventSub) { this.eventSub.unsubscribe(); } @@ -60,6 +63,9 @@ export class TransformableElement { } mouseZoom($event) { + if (typeof document === 'undefined') { + return; + } this.zoomSub = of($event).pipe( throttleTime(300), tap((event) => { @@ -102,7 +108,7 @@ export class TransformableElement { } mouseMove($event) { - if (this._mouseDown) { + if (this._mouseDown && typeof document !== 'undefined') { $event.stopPropagation(); $event.preventDefault(); this.translateX = this._originTranslateX + ($event['clientX'] - this._originMouseX); @@ -114,9 +120,11 @@ export class TransformableElement { } mouseUp($event) { - this._mouseDown = false; - document.body.style.cursor = 'default'; - this.element.style.cursor = 'grab'; + if (typeof document !== 'undefined') { + this._mouseDown = false; + document.body.style.cursor = 'default'; + this.element.style.cursor = 'grab'; + } } zoomOut(step = 0.25) { diff --git a/devui/input-number/demo/input-number-demo.component.html b/devui/input-number/demo/input-number-demo.component.html index 22618063..07182493 100755 --- a/devui/input-number/demo/input-number-demo.component.html +++ b/devui/input-number/demo/input-number-demo.component.html @@ -37,7 +37,7 @@
-
{{ 'components.input-number.DecimalLimitDemo.title' | translate }}
+
{{ 'components.input-number.decimalLimitDemo.title' | translate }}
diff --git a/devui/input-number/input-number.component.ts b/devui/input-number/input-number.component.ts index beed8b00..e7875ad8 100755 --- a/devui/input-number/input-number.component.ts +++ b/devui/input-number/input-number.component.ts @@ -1,3 +1,4 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, ChangeDetectorRef, @@ -5,6 +6,7 @@ import { ElementRef, EventEmitter, forwardRef, + Inject, Input, OnChanges, OnDestroy, @@ -60,6 +62,7 @@ export class InputNumberComponent implements ControlValueAccessor, OnChanges, On disabledDec = false; lastEmittedValue: number; lastValue: number; + document: Document; @Input() set min(val) { if (val || val === 0) { @@ -84,7 +87,10 @@ export class InputNumberComponent implements ControlValueAccessor, OnChanges, On private onChangeCallback = (v: any) => { } - constructor(private cdr: ChangeDetectorRef, private el: ElementRef, private renderer: Renderer2) { + + constructor(private cdr: ChangeDetectorRef, private el: ElementRef, private renderer: Renderer2, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } registerOnChange(fn: any): void { @@ -244,12 +250,22 @@ export class InputNumberComponent implements ControlValueAccessor, OnChanges, On } else { const decimals = this.getMaxDecimals(this.value); const floatValue = type === 'increase' ? (this.value + this.step) : (this.value - this.step); - this.updateValue(parseFloat(floatValue.toFixed(decimals))); + if (this.matchReg(floatValue + '')) { + this.updateValue(parseFloat(floatValue.toFixed(decimals))); + } } this.inputElement.nativeElement.focus(); } } + private matchReg(value) { + if (this.reg && !value.match(new RegExp(this.reg))) { + return false; + } else { + return true; + } + } + private canIncrease() { if (this.allowEmpty && (this.value === null || this.value === undefined)) { return (this.min + this.step) <= this.max; @@ -346,7 +362,7 @@ export class InputNumberComponent implements ControlValueAccessor, OnChanges, On this.setValue(this.lastValue); return; } - if (this.reg && !value.match(new RegExp(this.reg))) { + if (!this.matchReg(value)) { this.setValue(this.lastValue); return; } else if ( @@ -426,7 +442,7 @@ export class InputNumberComponent implements ControlValueAccessor, OnChanges, On } registerBlurListener() { - document.addEventListener('click', this.emitBlurEvent.bind(this), { + this.document.addEventListener('click', this.emitBlurEvent.bind(this), { capture: true, once: true, }); @@ -434,7 +450,7 @@ export class InputNumberComponent implements ControlValueAccessor, OnChanges, On emitBlurEvent(event: MouseEvent) { if (!this.disabled && this.el.nativeElement !== event.target && !this.el.nativeElement.contains(event.target)) { - const blurEvt = document.createEvent('Event'); + const blurEvt = this.document.createEvent('Event'); blurEvt.initEvent('blur', false, true); this.el.nativeElement.dispatchEvent(blurEvt); this.onTouchedCallback(); diff --git a/devui/loading/demo/custom/custom.component.html b/devui/loading/demo/custom/custom.component.html index 8fea4872..0e12f9fc 100755 --- a/devui/loading/demo/custom/custom.component.html +++ b/devui/loading/demo/custom/custom.component.html @@ -1,5 +1,5 @@
- Loading Style 2 + Loading Style 2 Loading Style 3 diff --git a/devui/loading/demo/full-screen/full-screen.component.ts b/devui/loading/demo/full-screen/full-screen.component.ts index 5f391fd9..bd374fb6 100644 --- a/devui/loading/demo/full-screen/full-screen.component.ts +++ b/devui/loading/demo/full-screen/full-screen.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject } from '@angular/core'; import { LoadingService } from 'ng-devui/loading'; @Component({ selector: 'd-full-screen', @@ -8,7 +9,7 @@ import { LoadingService } from 'ng-devui/loading'; export class FullScreenComponent { resultTarget: any; isShow: boolean; - constructor(private loadingService: LoadingService) {} + constructor(private loadingService: LoadingService, @Inject(DOCUMENT) private doc: any) {} openFullScreen() { /* @@ -23,7 +24,7 @@ export class FullScreenComponent { } openTargetLoading() { - const dm = document.querySelector('#me'); + const dm = this.doc.querySelector('#me'); this.resultTarget = this.loadingService.open({ target: dm, message: 'One moment please...', diff --git a/devui/loading/loading.service.ts b/devui/loading/loading.service.ts index 533dc3c6..dcb2982e 100644 --- a/devui/loading/loading.service.ts +++ b/devui/loading/loading.service.ts @@ -1,4 +1,5 @@ -import { ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; import { OverlayContainerRef } from 'ng-devui/overlay-container'; import { LoadingBackdropComponent } from './loading-backdrop.component'; import { LoadingComponent } from './loading.component'; @@ -8,16 +9,20 @@ import { ILoadingOptions } from './loading.types'; }) export class LoadingService { private renderer: Renderer2; + document: Document; + constructor( private overlayContainerRef: OverlayContainerRef, private componentFactoryResolver: ComponentFactoryResolver, - private rendererFactory: RendererFactory2 + private rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private doc: any ) { this.renderer = this.rendererFactory.createRenderer(null, null); + this.document = this.doc; } // loading 服务内的函数,外部就可以传入ILoadingOptions类型的参数调用它 open({ - target = document.body, + target = this.document.body, backdrop = true, message, loadingTemplateRef, @@ -39,7 +44,7 @@ export class LoadingService { Object.assign(backdropRef.instance, { backdrop: backdrop, zIndex: zIndex, - target: target ? target : document.body, + target: target ? target : this.document.body, }); const viewRef = backdropRef.hostView; (viewRef as EmbeddedViewRef).rootNodes.forEach((node) => target.appendChild(node)); @@ -57,7 +62,7 @@ export class LoadingService { top: view ? view.top : '50%', left: view ? view.left : '50%', isCustomPosition: !!view, - target: target ? target : document.body, + target: target ? target : this.document.body, }); this.renderer.setStyle(target, 'position', positionType); diff --git a/devui/mention/utils.ts b/devui/mention/utils.ts index 033faf17..6cef3e8a 100644 --- a/devui/mention/utils.ts +++ b/devui/mention/utils.ts @@ -40,9 +40,9 @@ const properties = [ const isBrowser = typeof window !== 'undefined'; const isFirefox = isBrowser && window['mozInnerScreenX'] != null; -const _parseInt = value => { +function _parseInt(value) { return parseInt(value, 10); -}; +} export function getCaretCoordinates(element, position, options?) { if (!isBrowser) { diff --git a/devui/modal/demo/basic-update/basic-update.component.html b/devui/modal/demo/basic-update/basic-update.component.html index dc106627..eddfd804 100644 --- a/devui/modal/demo/basic-update/basic-update.component.html +++ b/devui/modal/demo/basic-update/basic-update.component.html @@ -1 +1 @@ -click me! +click me! diff --git a/devui/modal/demo/basic/basic.component.html b/devui/modal/demo/basic/basic.component.html index cd5ff75c..c3b48a8e 100755 --- a/devui/modal/demo/basic/basic.component.html +++ b/devui/modal/demo/basic/basic.component.html @@ -1,2 +1,4 @@ open modal! -open modal without animation! +open modal without animation! diff --git a/devui/modal/demo/fixed/fixed-wrapper.component.ts b/devui/modal/demo/fixed/fixed-wrapper.component.ts index b0d13fcf..2e63eb57 100644 --- a/devui/modal/demo/fixed/fixed-wrapper.component.ts +++ b/devui/modal/demo/fixed/fixed-wrapper.component.ts @@ -57,7 +57,7 @@ export class FixedWrapperComponent { } setHtmlStyle() { - this.scrollTop = document.documentElement.scrollTop; + this.scrollTop = this.documentRef.documentElement.scrollTop; this.renderer.setStyle(this.documentRef.documentElement, 'top', `-${this.scrollTop}px`); this.renderer.setStyle(this.documentRef.documentElement, 'position', 'fixed'); this.renderer.setStyle(this.documentRef.documentElement, 'width', `100%`); @@ -69,6 +69,6 @@ export class FixedWrapperComponent { this.renderer.removeStyle(this.documentRef.documentElement, 'width'); this.renderer.removeStyle(this.documentRef.documentElement, 'overflow'); this.renderer.removeStyle(this.documentRef.documentElement, 'top'); - document.documentElement.scrollTop = this.scrollTop; + this.documentRef.documentElement.scrollTop = this.scrollTop; } } diff --git a/devui/modal/demo/hide/hide.component.html b/devui/modal/demo/hide/hide.component.html index 576b928c..06168c43 100644 --- a/devui/modal/demo/hide/hide.component.html +++ b/devui/modal/demo/hide/hide.component.html @@ -1 +1 @@ -click me! +click me! diff --git a/devui/modal/demo/template/template.component.html b/devui/modal/demo/template/template.component.html index bcfddd74..56573a14 100644 --- a/devui/modal/demo/template/template.component.html +++ b/devui/modal/demo/template/template.component.html @@ -1,6 +1,6 @@
- Dialog - Modal + Dialog + Modal
diff --git a/devui/modal/demo/tips/tips.component.html b/devui/modal/demo/tips/tips.component.html index 2c647bca..011a1c78 100755 --- a/devui/modal/demo/tips/tips.component.html +++ b/devui/modal/demo/tips/tips.component.html @@ -1,6 +1,6 @@
- success - fail - warning - info + success + fail + warning + info
diff --git a/devui/modal/dialog.service.ts b/devui/modal/dialog.service.ts index ad698471..5f8a9e8a 100755 --- a/devui/modal/dialog.service.ts +++ b/devui/modal/dialog.service.ts @@ -1,6 +1,8 @@ +import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, ComponentRef, + Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; @@ -15,11 +17,14 @@ import { IDialogOptions } from './modal.types'; export class DialogService { contentRef: ComponentRef; private renderer: Renderer2; + document: Document; constructor(private componentFactoryResolver: ComponentFactoryResolver, private overlayContainerRef: OverlayContainerRef, private rendererFactory: RendererFactory2, - private devConfigService: DevConfigService) { + private devConfigService: DevConfigService, + @Inject(DOCUMENT) private doc: any) { this.renderer = this.rendererFactory.createRenderer(null, null); + this.document = this.doc; } open({ @@ -114,14 +119,14 @@ export class DialogService { modalRef.instance.onHidden = () => { if (modalRef.instance.documentOverFlow) { - this.renderer.removeStyle(document.body, 'top'); - this.renderer.removeStyle(document.body, 'left'); - this.renderer.removeClass(document.body, 'devui-body-scrollblock'); - this.renderer.removeClass(document.body, 'devui-body-overflow-hidden'); - document.documentElement.scrollTop = modalRef.instance.scrollTop; - document.body.scrollTop = modalRef.instance.scrollTop; - document.documentElement.scrollLeft = modalRef.instance.scrollLeft; - document.body.scrollLeft = modalRef.instance.scrollLeft; + this.renderer.removeStyle(this.document.body, 'top'); + this.renderer.removeStyle(this.document.body, 'left'); + this.renderer.removeClass(this.document.body, 'devui-body-scrollblock'); + this.renderer.removeClass(this.document.body, 'devui-body-overflow-hidden'); + this.document.documentElement.scrollTop = modalRef.instance.scrollTop; + this.document.body.scrollTop = modalRef.instance.scrollTop; + this.document.documentElement.scrollLeft = modalRef.instance.scrollLeft; + this.document.body.scrollLeft = modalRef.instance.scrollLeft; } if (onClose) { onClose(); diff --git a/devui/modal/modal.component.ts b/devui/modal/modal.component.ts index 1f5955e4..bc3a7f89 100755 --- a/devui/modal/modal.component.ts +++ b/devui/modal/modal.component.ts @@ -1,6 +1,6 @@ -import { Component, ElementRef, Input, OnDestroy, OnInit, Renderer2, TemplateRef, ViewChild } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, ElementRef, Inject, Input, OnDestroy, OnInit, Renderer2, TemplateRef, ViewChild } from '@angular/core'; import { backdropFadeInOut, wipeInOutAnimation } from 'ng-devui/utils'; -import { DocumentRef } from 'ng-devui/window-ref'; import { isUndefined } from 'lodash-es'; import { fromEvent, Observable, Subscription } from 'rxjs'; import { ModalContainerDirective } from './modal.directive'; @@ -49,15 +49,17 @@ export class ModalComponent implements OnInit, OnDestroy { pressEscToClose: Subscription = new Subscription(); contentTemplate: TemplateRef; + document: Document; constructor( private elementRef: ElementRef, - private documentRef: DocumentRef, private renderer: Renderer2, + @Inject(DOCUMENT) private doc: any ) { this.backdropCloseable = isUndefined(this.backdropCloseable) ? true : this.backdropCloseable; + this.document = this.doc; } ngOnInit() { @@ -104,7 +106,7 @@ export class ModalComponent implements OnInit, OnDestroy { onModalClick = ($event) => { // 一定要document.contains($event.target),因为$event.target可能已经不在document里了,这个时候就不能进hide了,使用document.body兼容IE if (this.backdropCloseable && !this.ignoreBackDropClick && - (!this.dialogElement.nativeElement.contains($event.target) && document.body.contains($event.target))) { + (!this.dialogElement.nativeElement.contains($event.target) && this.document.body.contains($event.target))) { this.hide(); } this.ignoreBackDropClick = false; @@ -137,16 +139,16 @@ export class ModalComponent implements OnInit, OnDestroy { } show() { - if (document.documentElement.scrollHeight > document.documentElement.clientHeight) { + if (this.document.documentElement.scrollHeight > this.document.documentElement.clientHeight) { this.documentOverFlow = true; - this.scrollTop = document.documentElement.scrollTop || document.body.scrollTop; - this.scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; - this.renderer.addClass(this.documentRef.body, 'devui-body-scrollblock'); - this.renderer.setStyle(this.documentRef.body, 'top', `-${this.scrollTop}px`); - this.renderer.setStyle(this.documentRef.body, 'left', `-${this.scrollLeft}px`); + this.scrollTop = this.document.documentElement.scrollTop || this.document.body.scrollTop; + this.scrollLeft = this.document.documentElement.scrollLeft || this.document.body.scrollLeft; + this.renderer.addClass(this.document.body, 'devui-body-scrollblock'); + this.renderer.setStyle(this.document.body, 'top', `-${this.scrollTop}px`); + this.renderer.setStyle(this.document.body, 'left', `-${this.scrollLeft}px`); } if (!this.bodyScrollable && this.documentOverFlow) { - this.renderer.addClass(document.body, 'devui-body-overflow-hidden'); + this.renderer.addClass(this.document.body, 'devui-body-overflow-hidden'); } this.dialogElement.nativeElement.focus(); diff --git a/devui/modal/modal.service.ts b/devui/modal/modal.service.ts index 846aae39..252b75be 100755 --- a/devui/modal/modal.service.ts +++ b/devui/modal/modal.service.ts @@ -1,5 +1,7 @@ +import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, + Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; @@ -12,10 +14,14 @@ import { IModalOptions } from './modal.types'; @Injectable() export class ModalService { private renderer: Renderer2; + document: Document; + constructor(private componentFactoryResolver: ComponentFactoryResolver, private overlayContainerRef: OverlayContainerRef, private rendererFactory: RendererFactory2, - private devConfigService: DevConfigService) { + private devConfigService: DevConfigService, + @Inject(DOCUMENT) private doc: any) { this.renderer = this.rendererFactory.createRenderer(null, null); + this.document = this.doc; } open({ @@ -88,15 +94,15 @@ export class ModalService { modalRef.instance.onHidden = () => { if (modalRef.instance.documentOverFlow) { - this.renderer.removeStyle(document.body, 'top'); - this.renderer.removeStyle(document.body, 'left'); - this.renderer.removeClass(document.body, 'devui-body-scrollblock'); - this.renderer.removeClass(document.body, 'devui-body-overflow-hidden'); - document.documentElement.scrollTop = modalRef.instance.scrollTop; - document.body.scrollTop = modalRef.instance.scrollTop; - document.documentElement.scrollLeft = modalRef.instance.scrollLeft; - document.body.scrollLeft = modalRef.instance.scrollLeft; - } + this.renderer.removeStyle(this.document.body, 'top'); + this.renderer.removeStyle(this.document.body, 'left'); + this.renderer.removeClass(this.document.body, 'devui-body-scrollblock'); + this.renderer.removeClass(this.document.body, 'devui-body-overflow-hidden'); + this.document.documentElement.scrollTop = modalRef.instance.scrollTop; + this.document.body.scrollTop = modalRef.instance.scrollTop; + this.document.documentElement.scrollLeft = modalRef.instance.scrollLeft; + this.document.body.scrollLeft = modalRef.instance.scrollLeft; + } if (onClose) { onClose(); } diff --git a/devui/modal/movable.directive.ts b/devui/modal/movable.directive.ts index ee63c062..36526e2f 100755 --- a/devui/modal/movable.directive.ts +++ b/devui/modal/movable.directive.ts @@ -1,4 +1,4 @@ -import {Directive, ElementRef, HostListener, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; +import { Directive, ElementRef, HostListener, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; @Directive({ selector: '[dMovable]' @@ -48,7 +48,7 @@ export class MovableDirective implements OnInit, OnChanges { @HostListener('document:mousemove', ['$event']) onMouseMove(event: MouseEvent) { - if (this.md && this._allowDrag) { + if (typeof window !== 'undefined' && this.md && this._allowDrag) { // 阻止拖动过程中文字被选中 event.stopPropagation(); event.preventDefault(); diff --git a/devui/multi-auto-complete/demo/array/multi-auto-complete-demo-array.component.ts b/devui/multi-auto-complete/demo/array/multi-auto-complete-demo-array.component.ts index 8636de04..d5985944 100644 --- a/devui/multi-auto-complete/demo/array/multi-auto-complete-demo-array.component.ts +++ b/devui/multi-auto-complete/demo/array/multi-auto-complete-demo-array.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, OnInit } from '@angular/core'; import { of } from 'rxjs'; @Component({ @@ -16,6 +17,7 @@ export class MultiAutoCompleteDemoArrayComponent implements OnInit { msgs: Array = []; + constructor(@Inject(DOCUMENT) private doc: any) {} ngOnInit() { this.multiItems2 = this.multiItems1.map((lang, index) => ({ label: lang, id: index })); } @@ -37,13 +39,13 @@ export class MultiAutoCompleteDemoArrayComponent implements OnInit { } copy() { - const tempInput = document.createElement('input'); + const tempInput = this.doc.createElement('input'); tempInput.value = this.multiItems2.map(item => item.label).join(', '); - document.body.appendChild(tempInput); + this.doc.body.appendChild(tempInput); tempInput.select(); // 选择对象 - document.execCommand('Copy'); // 执行浏览器复制命令 + this.doc.execCommand('Copy'); // 执行浏览器复制命令 tempInput.style.display = 'none'; - document.body.removeChild(tempInput); + this.doc.body.removeChild(tempInput); this.msgs = [{ severity: 'success', summary: 'Copy success', detail: 'The data has been successfully copied to the clipboard.' }]; } } diff --git a/devui/nav-sprite/nav-sprite.component.ts b/devui/nav-sprite/nav-sprite.component.ts index aa5a6f50..7048e3c2 100644 --- a/devui/nav-sprite/nav-sprite.component.ts +++ b/devui/nav-sprite/nav-sprite.component.ts @@ -1,9 +1,11 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, + Inject, Input, OnDestroy, OnInit, @@ -96,7 +98,12 @@ export class NavSpriteComponent implements OnInit, AfterViewInit, OnDestroy { timeGap = 60; + document: Document; + get baseUrl() { + if (typeof window === 'undefined') { + return ''; + } return window.location.href.replace(window.location.hash, ''); } @@ -106,8 +113,11 @@ export class NavSpriteComponent implements OnInit, AfterViewInit, OnDestroy { private router: Router, private activeRout: ActivatedRoute, private render2: Renderer2, - private cdr: ChangeDetectorRef - ) {} + private cdr: ChangeDetectorRef, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } ngOnInit() { this.currentTemp = this.mode === 'default' ? this.defaultTemp : this.spriteTemp; // 设置当前的模式 @@ -117,7 +127,7 @@ export class NavSpriteComponent implements OnInit, AfterViewInit, OnDestroy { ngAfterViewInit() { setTimeout(() => { - const container = this.targetContainer === document.documentElement ? window : this.targetContainer; + const container = this.targetContainer === this.document.documentElement ? window : this.targetContainer; this.scrollSub = fromEvent(container, 'scroll') .pipe(debounceTime(300)) .subscribe(() => { diff --git a/devui/package.json b/devui/package.json index 8dbdf669..cbdb7ddc 100755 --- a/devui/package.json +++ b/devui/package.json @@ -1,6 +1,6 @@ { "name": "ng-devui", - "version": "11.3.0", + "version": "11.4.0", "license": "MIT", "description": "DevUI components based on Angular", "keywords": [ diff --git a/devui/pagination/demo/additional/additional.component.html b/devui/pagination/demo/additional/additional.component.html index 5d6e4542..6a0d818f 100755 --- a/devui/pagination/demo/additional/additional.component.html +++ b/devui/pagination/demo/additional/additional.component.html @@ -47,8 +47,8 @@

Default Mode

> total = 0 - total = 5 - total = 15 + total = 5 + total = 15
@@ -65,10 +65,10 @@

Simple Mode

> total = 0 - total = 20 - total = 30000 - total = 100000 - index = 2 - index = 3 + total = 20 + total = 30000 + total = 100000 + index = 2 + index = 3
diff --git a/devui/pagination/doc/api-en.md b/devui/pagination/doc/api-en.md index 4dfb3ae3..e5ac1e37 100644 --- a/devui/pagination/doc/api-en.md +++ b/devui/pagination/doc/api-en.md @@ -38,7 +38,7 @@ In the page: | showPageSelector | `boolean` | true | Optional. Whether to display the page number drop-down list in simplified mode. | [Simplified mode](demo#minimalist-model) | | haveConfigMenu | `boolean` | false | Optional. Whether to display the configuration in simplified mode | [Simplified mode](demo#minimalist-model) | | autoFixPageIndex | `boolean` | true | Optional. Indicates whether to automatically correct the page number when the page size is changed. If the pageIndex is processed in the `pageSizeChange` event, you are advised to set the value to `false` | [Simplified Mode](demo#minimalist-model) | -| autoHide | `boolean` | false | Optional, whether to hide automatically, autoHide is true minimum value of pageSizeOptions > total do not show pagination | [Simplified Mode](demo#minimalist-model) |. +| autoHide | `boolean` | false | Optional, whether to hide automatically, autoHide is true minimum value of pageSizeOptions > total do not show pagination | [Simplified Mode](demo#minimalist-model) | ## d-pagination event diff --git a/devui/pagination/pagination.component.ts b/devui/pagination/pagination.component.ts index 21732d43..9ac6bde8 100755 --- a/devui/pagination/pagination.component.ts +++ b/devui/pagination/pagination.component.ts @@ -103,7 +103,7 @@ export class PaginationComponent implements OnChanges, AfterViewInit, OnDestroy, @Input() showPageSelector = true; @Input() haveConfigMenu = false; @Input() autoFixPageIndex = true; - /** + /** * 是否自动隐藏 */ @Input() autoHide = false; diff --git a/devui/panel/demo/basic/basic.component.html b/devui/panel/demo/basic/basic.component.html index 313730c4..a16f8af4 100755 --- a/devui/panel/demo/basic/basic.component.html +++ b/devui/panel/demo/basic/basic.component.html @@ -7,6 +7,14 @@ This is body

+ + + Panel has no left padding + + + This is body + +

Panel with header and footer This is body diff --git a/devui/panel/doc/api-cn.md b/devui/panel/doc/api-cn.md index 75de3e72..ba0464ed 100644 --- a/devui/panel/doc/api-cn.md +++ b/devui/panel/doc/api-cn.md @@ -1,11 +1,13 @@ # 如何使用 -在module中引入: +在 module 中引入: + ```ts import { PanelModule } from 'ng-devui'; ``` 在页面中使用: + ```html @@ -15,15 +17,17 @@ import { PanelModule } from 'ng-devui'; ``` # d-panel + ## d-panel 参数 -| 参数 | 类型 | 默认 | 说明 | 跳转 Demo |全局配置项| -| :----------------: | :----------: | :-----------------------------: | :-------: | :----------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| type | [`PanelType`](#paneltype) | 'default' | 可选,面板的类型 | [基本用法](demo#basic-usage) | -| cssClass | `string` | -- | 可选,自定义 class 名 | -| isCollapsed | `boolean` | false | 可选,是否展开 | [基本用法](demo#basic-usage) | -| showAnimation | `boolean` | true | 可选,是否展示动画 | [基本用法](demo#basic-usage) | -| beforeToggle | `Function\|Promise\|Observable` | -- | 可选,面板折叠状态改变前的回调函数,返回 boolean 类型,返回 false 可以阻止面板改变折叠状态 | [根据条件阻止折叠](demo#condition-change) | +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| :------------: | :-----------------------------: | :-------: | :----------------------------------------------------------------------------------------: | :---------------------------------------- | ---------- | +| type | [`PanelType`](#paneltype) | 'default' | 可选,面板的类型 | [基本用法](demo#basic-usage) | +| cssClass | `string` | -- | 可选,自定义 class 名 | +| isCollapsed | `boolean` | false | 可选,是否展开 | [基本用法](demo#basic-usage) | +| hasLeftPadding | `boolean` | true | 可选,是否显示左侧填充 | [基本用法](demo#basic-usage) | +| showAnimation | `boolean` | true | 可选,是否展示动画 | [基本用法](demo#basic-usage) | +| beforeToggle | `Function\|Promise\|Observable` | -- | 可选,面板折叠状态改变前的回调函数,返回 boolean 类型,返回 false 可以阻止面板改变折叠状态 | [根据条件阻止折叠](demo#condition-change) | ## d-panel 事件 @@ -34,6 +38,7 @@ import { PanelModule } from 'ng-devui'; # 接口 & 类型定义 ### PanelType + ```ts -export type PanelType = 'default' | 'primary' | 'success' | 'danger' | 'warning' | 'info'; -``` \ No newline at end of file +export type PanelType = 'default' | 'primary' | 'success' | 'danger' | 'warning' | 'info'; +``` diff --git a/devui/panel/doc/api-en.md b/devui/panel/doc/api-en.md index 53249936..1ec46255 100644 --- a/devui/panel/doc/api-en.md +++ b/devui/panel/doc/api-en.md @@ -17,15 +17,17 @@ In the page: ``` # d-panel + ## d-panel Parameters -| Parameter | Type | Default | Description | Jump to Demo |Global Config| -| :----------------: | :----------: | :-----------------------------: | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -| type | [`PanelType`](#paneltype) | 'default' | Optional. Panel type | [Basic Usage](demo#basic-usage) | -| cssClass | `string` | -- | Optional. User-defined class name | -| isCollapsed | `boolean` | false | Optional. Whether to expand the file | [Basic Usage](demo#basic-usage) | -| showAnimation | `boolean` | true | Optional. Indicating whether to display animations. | [Basic Usage](demo#basic-usage) | -| beforeToggle | `Function\|Promise\|Observable` | -- | Optional. Callback function before the panel folding status changes. The value of this parameter is of the boolean type. If false is returned, the panel folding status changes. | [Prevent Collapse Based on Conditions](demo#condition-change) | +| Parameter | Type | Default | Description | Jump to Demo | Global Config | +| :------------: | :-----------------------------: | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------ | ------------- | +| type | [`PanelType`](#paneltype) | 'default' | Optional. Panel type | [Basic Usage](demo#basic-usage) | +| cssClass | `string` | -- | Optional. User-defined class name | +| isCollapsed | `boolean` | false | Optional. Whether to expand the panel | [Basic Usage](demo#basic-usage) | +| hasLeftPadding | `boolean` | true | Optional. Whether to display the left padding | [Basic Usage](demo#basic-usage) | +| showAnimation | `boolean` | true | Optional. Indicating whether to display animations. | [Basic Usage](demo#basic-usage) | +| beforeToggle | `Function\|Promise\|Observable` | -- | Optional. Callback function before the panel folding status changes. The value of this parameter is of the boolean type. If false is returned, the panel folding status changes. | [Prevent Collapse Based on Conditions](demo#condition-change) | ## d-panel Event @@ -36,6 +38,7 @@ In the page: # Interface & Type Definition ### PanelType + ```ts -export type PanelType = 'default' | 'primary' | 'success' | 'danger' | 'warning' | 'info'; -``` \ No newline at end of file +export type PanelType = 'default' | 'primary' | 'success' | 'danger' | 'warning' | 'info'; +``` diff --git a/devui/panel/panel.component.html b/devui/panel/panel.component.html index 39eeb623..116f0d3f 100755 --- a/devui/panel/panel.component.html +++ b/devui/panel/panel.component.html @@ -4,7 +4,7 @@
boolean | Promise | Observable; @Output() toggle: EventEmitter = new EventEmitter(); diff --git a/devui/popover/demo/basic/basic.component.css b/devui/popover/demo/basic/basic.component.css deleted file mode 100755 index f1a2fe0b..00000000 --- a/devui/popover/demo/basic/basic.component.css +++ /dev/null @@ -1,4 +0,0 @@ -.btn-group > d-button { - margin-right: 5px; - margin-bottom: 5px; -} diff --git a/devui/popover/demo/basic/basic.component.html b/devui/popover/demo/basic/basic.component.html index 29538806..1efc3325 100755 --- a/devui/popover/demo/basic/basic.component.html +++ b/devui/popover/demo/basic/basic.component.html @@ -1,10 +1,19 @@
- + default info - + error - + warning success - No Animation + No Animation
diff --git a/devui/popover/demo/basic/basic.component.scss b/devui/popover/demo/basic/basic.component.scss new file mode 100644 index 00000000..57931071 --- /dev/null +++ b/devui/popover/demo/basic/basic.component.scss @@ -0,0 +1,16 @@ +@import '~ng-devui/styles-var/devui-var.scss'; + +.btn-group > d-button { + margin-right: 5px; + margin-bottom: 5px; +} + +:host ::ng-deep .devui-btn-success { + background: $devui-success !important; + color: $devui-base-bg; +} + +:host ::ng-deep .devui-btn-warning { + background: $devui-warning !important; + color: $devui-base-bg; +} diff --git a/devui/popover/demo/basic/basic.component.ts b/devui/popover/demo/basic/basic.component.ts index 31c7f5e1..fb413981 100755 --- a/devui/popover/demo/basic/basic.component.ts +++ b/devui/popover/demo/basic/basic.component.ts @@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'd-basic', templateUrl: './basic.component.html', - styleUrls: ['./basic.component.css'] + styleUrls: ['./basic.component.scss'] }) export class BasicComponent implements OnInit { constructor() { } diff --git a/devui/popover/demo/customize/customize.component.html b/devui/popover/demo/customize/customize.component.html index 939183cf..9724776d 100644 --- a/devui/popover/demo/customize/customize.component.html +++ b/devui/popover/demo/customize/customize.component.html @@ -11,6 +11,7 @@ MouseEnter delay 500ms - + MouseLeave delay 2000ms diff --git a/devui/popover/demo/popover-demo.component.ts b/devui/popover/demo/popover-demo.component.ts index 0fc94032..32bcbfb8 100755 --- a/devui/popover/demo/popover-demo.component.ts +++ b/devui/popover/demo/popover-demo.component.ts @@ -10,7 +10,7 @@ export class PopoverDemoComponent implements OnInit, OnDestroy { basicSource: Array = [ { title: 'HTML', language: 'xml', code: require('!!raw-loader!./basic/basic.component.html') }, { title: 'TS', language: 'typescript', code: require('!!raw-loader!./basic/basic.component.ts') }, - { title: 'SCSS', language: 'css', code: require('!!raw-loader!./basic/basic.component.css') }, + { title: 'SCSS', language: 'css', code: require('!!raw-loader!./basic/basic.component.scss') }, ]; positionSource: Array = [ diff --git a/devui/popover/demo/scroll-element/scroll-element.component.ts b/devui/popover/demo/scroll-element/scroll-element.component.ts index 5d34d429..64b881f1 100644 --- a/devui/popover/demo/scroll-element/scroll-element.component.ts +++ b/devui/popover/demo/scroll-element/scroll-element.component.ts @@ -1,10 +1,11 @@ -import { Component } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject } from '@angular/core'; @Component({ selector: 'd-scroll-element', templateUrl: './scroll-element.component.html' }) export class ScrollElementComponent { - scrollElement: Element = document.querySelector('.doc-viewer-container'); - constructor() { + scrollElement: Element = this.doc.querySelector('.doc-viewer-container'); + constructor(@Inject(DOCUMENT) private doc: any) { } } diff --git a/devui/popover/popover.component.ts b/devui/popover/popover.component.ts index a64f7c66..3f13c34e 100755 --- a/devui/popover/popover.component.ts +++ b/devui/popover/popover.component.ts @@ -14,11 +14,10 @@ import { TemplateRef } from '@angular/core'; import { PositionService } from 'ng-devui/position'; -import { PositionType } from 'ng-devui/tooltip'; import { directionFadeInOut } from 'ng-devui/utils'; import { fromEvent, Subscription } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { PopoverType } from './popover.types'; +import { PopoverType, PositionType } from './popover.types'; interface PopoverStyle { backgroundColor?: string; @@ -32,32 +31,15 @@ interface PopoverStyle { }) export class PopoverComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { @Input() triggerElementRef: ElementRef; - currentPosition = 'top'; + currentPosition: PositionType = 'top'; connectionBias: string; - _position: PositionType | PositionType[] = 'top'; + _position: PositionType | PositionType[] = ['top', 'left', 'bottom', 'right']; @Input() get position() { return this._position; } set position(pos) { this._position = pos; - let placementPrimary: string; - let placementSecondary: string; - if (Array.isArray(pos)) { - placementPrimary = pos[0].split('-')[0] || 'top'; - placementSecondary = pos[1].split('-')[1] || 'center'; - } else { - placementPrimary = pos?.split('-')[0] || 'top'; - placementSecondary = pos?.split('-')[1] || 'center'; - } - this.currentPosition = placementPrimary; - this.connectionBias = `bias-${placementSecondary}`; - if (placementSecondary === 'center') { - if (placementPrimary === 'left' || placementPrimary === 'right') { - this.connectionBias = 'bias-vertical-center'; - } else { - this.connectionBias = 'bias-horizontal-center'; - } - } + this.currentPosition = Array.isArray(pos) ? pos[0] : pos; } @Input() content: string | HTMLElement | TemplateRef; @Input() showAnimation = true; @@ -142,9 +124,7 @@ export class PopoverComponent implements OnInit, AfterViewInit, OnDestroy, OnCha } } - show() { - this.animateState = Array.isArray(this.position) ? this.position[0] : this.position; - } + show() {} hide() { this.animateState = 'void'; @@ -167,14 +147,32 @@ export class PopoverComponent implements OnInit, AfterViewInit, OnDestroy, OnCha } updatePosition() { + this.renderer.setStyle(this.elementRef.nativeElement, 'visibility', 'hidden'); + this.renderer.setStyle(this.elementRef.nativeElement, 'transform', 'translate(0, -99999px)'); const rect = this.positionService.positionElements( this.triggerElementRef.nativeElement, this.elementRef.nativeElement, this.position, this.appendToBody ); - this.renderer.setStyle(this.elementRef.nativeElement, 'left', `${rect.left}px`); - this.renderer.setStyle(this.elementRef.nativeElement, 'top', `${rect.top}px`); + setTimeout(() => { + // 预防脏检查 + this.currentPosition = rect.placementPrimary; + this.animateState = this.currentPosition; + this.connectionBias = `bias-${rect.placementSecondary}`; + if (rect.placementSecondary === 'center') { + if (rect.placementPrimary === 'left' || rect.placementPrimary === 'right') { + this.connectionBias = 'bias-vertical-center'; + } else { + this.connectionBias = 'bias-horizontal-center'; + } + } + this.renderer.setStyle(this.elementRef.nativeElement, 'left', `${rect.left}px`); + this.renderer.setStyle(this.elementRef.nativeElement, 'top', `${rect.top}px`); + // 移除样式 + this.renderer.removeStyle(this.elementRef.nativeElement, 'visibility'); + this.renderer.removeStyle(this.elementRef.nativeElement, 'transform'); + }); } public updatePositionAndDetectChange() { diff --git a/devui/popover/popover.directive.ts b/devui/popover/popover.directive.ts index f5416e48..7f38df4e 100755 --- a/devui/popover/popover.directive.ts +++ b/devui/popover/popover.directive.ts @@ -1,8 +1,10 @@ +import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, ComponentRef, Directive, ElementRef, + Inject, Injector, Input, OnDestroy, @@ -97,6 +99,7 @@ export class PopoverDirective implements OnInit, OnDestroy { isEnter: boolean; unsubscribe$ = new Subject(); unsubscribeP$ = new Subject(); + document: Document; @Input() set visible(_isShow: boolean) { if (_isShow) { // when set value and create component at the same time,should wait after ng2 dirty check done @@ -117,7 +120,10 @@ export class PopoverDirective implements OnInit, OnDestroy { private injector: Injector, private componentFactoryResolver: ComponentFactoryResolver, private devConfigService: DevConfigService, - ) {} + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } onDocumentClick = (event) => { event.stopPropagation(); @@ -197,7 +203,7 @@ export class PopoverDirective implements OnInit, OnDestroy { } this.popoverComponentRef.instance.show(); - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); } destroy() { @@ -205,7 +211,7 @@ export class PopoverDirective implements OnInit, OnDestroy { this.popoverComponentRef.destroy(); this.popoverComponentRef = null; } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); if (this.unsubscribeP$) { this.unsubscribeP$.next(); this.unsubscribeP$.complete(); diff --git a/devui/portal/portal.component.ts b/devui/portal/portal.component.ts index db1b750b..824db30b 100755 --- a/devui/portal/portal.component.ts +++ b/devui/portal/portal.component.ts @@ -1,12 +1,13 @@ +import { DOCUMENT } from '@angular/common'; import { - ApplicationRef, - Component, - EmbeddedViewRef, - TemplateRef, - ViewChild, + ApplicationRef, + Component, + EmbeddedViewRef, + Inject, + TemplateRef, + ViewChild } from '@angular/core'; - -import {forEach} from 'lodash-es'; +import { forEach } from 'lodash-es'; @Component({ selector: 'd-portal', @@ -20,18 +21,20 @@ export class PortalComponent { viewRef: EmbeddedViewRef; portalContainer: HTMLElement; @ViewChild('templateRef', { static: true }) templateRef: TemplateRef; + document: Document; - constructor(private appRef: ApplicationRef) { + constructor(private appRef: ApplicationRef, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } addContent() { - this.portalContainer = document.createElement('div'); + this.portalContainer = this.document.createElement('div'); this.viewRef = this.templateRef.createEmbeddedView(this); forEach(this.viewRef.rootNodes, (node) => { this.portalContainer.appendChild(node); }); this.appRef.attachView(this.viewRef); - document.body.appendChild(this.portalContainer); + this.document.body.appendChild(this.portalContainer); } open() { @@ -41,7 +44,7 @@ export class PortalComponent { close() { if (this.viewRef && this.portalContainer) { - document.body.removeChild(this.portalContainer); + this.document.body.removeChild(this.portalContainer); this.viewRef.destroy(); this.viewRef = null; this.portalContainer = null; diff --git a/devui/position/positioning.service.ts b/devui/position/positioning.service.ts index 89716322..01a49789 100755 --- a/devui/position/positioning.service.ts +++ b/devui/position/positioning.service.ts @@ -222,15 +222,16 @@ export class PositionService { private isInViewPort(ele, {offsetLeft, offsetTop}) { const targetElBCR = ele.getBoundingClientRect(); - const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; - const viewPortWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + const viewPortHeight = this.windowRef.innerHeight || this.documentRef.documentElement.clientHeight + || this.documentRef.body.clientHeight; + const viewPortWidth = this.windowRef.innerWidth || this.documentRef.documentElement.clientWidth || this.documentRef.body.clientWidth; const height = targetElBCR.height || targetElBCR.offsetHeight; const width = targetElBCR.width || targetElBCR.offsetWidth; offsetTop = offsetTop || ele.offsetTop; - const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; + const scrollTop = this.documentRef.documentElement.scrollTop || this.documentRef.body.scrollTop; const top = offsetTop - scrollTop; offsetLeft = offsetLeft || ele.offsetLeft; - const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; + const scrollLeft = this.documentRef.documentElement.scrollLeft || this.documentRef.body.scrollLeft; const left = offsetLeft - scrollLeft; return top + height <= viewPortHeight && top > 0 && left + width <= viewPortWidth && left > 0; diff --git a/devui/quadrant-diagram/demo/basic/basic.component.scss b/devui/quadrant-diagram/demo/basic/basic.component.scss index 24656e2a..90fbe057 100644 --- a/devui/quadrant-diagram/demo/basic/basic.component.scss +++ b/devui/quadrant-diagram/demo/basic/basic.component.scss @@ -70,7 +70,7 @@ section { font-size: $devui-font-size-card-title; color: $devui-text; background: $devui-base-bg; - box-shadow: $devui-shadow-length-base rgba(41, 48, 64, 0.25), $devui-shadow-length-base rgba(41, 48, 64, 0.2); + box-shadow: $devui-shadow-length-base $devui-light-shadow; border-radius: $devui-border-radius; width: 120px; margin: 10px; diff --git a/devui/quadrant-diagram/demo/config/config.component.scss b/devui/quadrant-diagram/demo/config/config.component.scss index 86246494..076a25d6 100644 --- a/devui/quadrant-diagram/demo/config/config.component.scss +++ b/devui/quadrant-diagram/demo/config/config.component.scss @@ -68,7 +68,7 @@ section { font-size: $devui-font-size-card-title; color: $devui-text; background: $devui-base-bg; - box-shadow: $devui-shadow-length-base rgba(41, 48, 64, 0.25), $devui-shadow-length-base rgba(41, 48, 64, 0.2); + box-shadow: $devui-shadow-length-base $devui-light-shadow; border-radius: $devui-border-radius; width: 120px; margin: 10px; diff --git a/devui/quadrant-diagram/demo/config/config.component.ts b/devui/quadrant-diagram/demo/config/config.component.ts index 196071f2..6acbfc7f 100644 --- a/devui/quadrant-diagram/demo/config/config.component.ts +++ b/devui/quadrant-diagram/demo/config/config.component.ts @@ -35,25 +35,25 @@ export class ConfigComponent implements OnInit { title: 'Perfect', backgroundColor: 'rgba(232,240,253,0.4)', - color: 'rgba(81,112,255,0.3)' + color: 'rgba(81,112,255,0.5)' }, { title: 'Excellent', backgroundColor: 'rgba(232,240,253,0.2)', - color: 'rgba(81,112,255,0.3)' + color: 'rgba(81,112,255,0.5)' }, { title: 'Keep it up', backgroundColor: 'rgba(243,246,248,0.4)', - color: 'rgba(149,158,178,0.3)' + color: 'rgba(149,158,178,0.5)' }, { title: 'Full of potential', backgroundColor: 'rgba(232,240,253,0.2)', - color: 'rgba(81,112,255,0.3)' + color: 'rgba(81,112,255,0.5)' }, ]; labelData = [{ title: 'Rose', x: 80, y: 20, content: '

Rose的能力

能力值:20

潜力值:80

', id: 'Rose' }]; diff --git a/devui/quadrant-diagram/quadrant-axis/quadrant-axis.component.ts b/devui/quadrant-diagram/quadrant-axis/quadrant-axis.component.ts index a484ee69..4d4ca7f1 100644 --- a/devui/quadrant-diagram/quadrant-axis/quadrant-axis.component.ts +++ b/devui/quadrant-diagram/quadrant-axis/quadrant-axis.component.ts @@ -34,11 +34,13 @@ export class QuadrantDiagramAxisComponent implements OnInit, OnChanges { } } ngOnInit(): void { + if (typeof window !== 'undefined') { this.themeService = window['devuiThemeService']; if (this.themeService && this.themeService.eventBus) { this.themeService.eventBus.add('themeChanged', this.refreshColor); } this.refreshColor(); + } } refreshColor = () => { diff --git a/devui/quadrant-diagram/quadrant-diagram.component.scss b/devui/quadrant-diagram/quadrant-diagram.component.scss index 23949dc3..6fb1bff5 100644 --- a/devui/quadrant-diagram/quadrant-diagram.component.scss +++ b/devui/quadrant-diagram/quadrant-diagram.component.scss @@ -1,5 +1,6 @@ @import '../style/theme/color'; @import '../style/theme/shadow'; +@import '../style/theme/color'; .devui-quadrant-diagram { position: relative; diff --git a/devui/quadrant-diagram/quadrant-diagram.component.ts b/devui/quadrant-diagram/quadrant-diagram.component.ts index 375999e8..29ab935a 100644 --- a/devui/quadrant-diagram/quadrant-diagram.component.ts +++ b/devui/quadrant-diagram/quadrant-diagram.component.ts @@ -61,7 +61,7 @@ export class QuadrantDiagramComponent implements OnInit, OnChanges { } launchFullscreen({ isFullscreen }) { - if (isFullscreen) { + if (typeof window !== 'undefined' && isFullscreen) { this.isFullScreen = isFullscreen; this.view = { height: window.screen.height, diff --git a/devui/quadrant-diagram/quadrant-diagram.service.ts b/devui/quadrant-diagram/quadrant-diagram.service.ts index 963fd351..f158d14b 100644 --- a/devui/quadrant-diagram/quadrant-diagram.service.ts +++ b/devui/quadrant-diagram/quadrant-diagram.service.ts @@ -1,14 +1,18 @@ -import { Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; @Injectable() export class QuadrantDiagramService { + document: Document; - constructor() { } + constructor(@Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } showAxisLine(x, y, diagramId, view, axisConfigs) { - const horizontalLine = document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-horizontal-line') as HTMLElement; - const verticalLine = document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-vertical-line') as HTMLElement; - const labelXAxisValue = document.querySelector('d-quadrant-diagram#' + diagramId + ' #devui-label-x-axis-value') as HTMLElement; - const labelYAxisValue = document.querySelector('d-quadrant-diagram#' + diagramId + ' #devui-label-y-axis-value') as HTMLElement; + const horizontalLine = this.document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-horizontal-line') as HTMLElement; + const verticalLine = this.document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-vertical-line') as HTMLElement; + const labelXAxisValue = this.document.querySelector('d-quadrant-diagram#' + diagramId + ' #devui-label-x-axis-value') as HTMLElement; + const labelYAxisValue = this.document.querySelector('d-quadrant-diagram#' + diagramId + ' #devui-label-y-axis-value') as HTMLElement; labelXAxisValue.textContent = this.getXAxisValue(view, axisConfigs, x); labelYAxisValue.textContent = this.getYAxisValue(view, axisConfigs, y); horizontalLine.style.top = y + 'px'; @@ -17,13 +21,13 @@ export class QuadrantDiagramService { verticalLine.style.display = ''; } hideAxisLine(diagramId) { - const horizontalLine = document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-horizontal-line') as HTMLElement; - const verticalLine = document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-vertical-line') as HTMLElement; + const horizontalLine = this.document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-horizontal-line') as HTMLElement; + const verticalLine = this.document.querySelector('d-quadrant-diagram#' + diagramId + ' .devui-vertical-line') as HTMLElement; verticalLine.style.display = 'none'; horizontalLine.style.display = 'none'; } setListPointerEvents(diagramId, value) { - const ele = document.querySelectorAll('d-quadrant-diagram#' + diagramId + ' .devui-list-style'); + const ele = this.document.querySelectorAll('d-quadrant-diagram#' + diagramId + ' .devui-list-style'); ele.forEach(element => { (element as HTMLElement).style.pointerEvents = value; }); diff --git a/devui/radio/doc/api-cn.md b/devui/radio/doc/api-cn.md index a2e7e13f..cc288f0b 100644 --- a/devui/radio/doc/api-cn.md +++ b/devui/radio/doc/api-cn.md @@ -20,7 +20,7 @@ import { RadioModule } from 'ng-devui'; | :----------------: | :----------: | :-----------------------------: | :---: | :------------------------------------------------------------------------------ | ------------------------------------ | | name | `string` | -- | 必选,单选项名称 | [互相独立的单选项](demo#basic-usage) | | value | `string` | -- | 必选,单选项值 | [互相独立的单选项](demo#basic-usage) | -| disabled | `boolean` | false | 可选,是否禁用该单选项 | [禁用](demo#disabled) | | +| disabled | `boolean` | false | 可选,是否禁用该单选项 | [禁用](demo#disabled) | | beforeChange | `Function \| Promise \| Observable` | -- | 可选,radio 切换前的回调函数,返回 boolean 类型,返回 false 可以阻止 radio 切换 | [回调切换](demo#condition-change) | ## d-radio 事件 @@ -36,7 +36,8 @@ import { RadioModule } from 'ng-devui'; | 参数 | 类型 | 默认 | 说明 | 跳转 Demo | | :----------: | :-----------------------------: | :------: | :-------------------------------------------------------------------------------------------: | -------------------------------------- | | name | `string` | -- | 必选,单选项名称 (radio 唯一标识符) | [竖向排列](demo#vertical) | -| value | `array` | -- | 必选,单选数据组 | [竖向排列](demo#vertical) | +| values | `array` | -- | 必选,单选数据组 | [竖向排列](demo#vertical) | +| disabled | `boolean` | false | 可选,是否禁用该选项组 | [radio-group根据条件终止切换操作](demo#condition-radio-group) | | cssStyle | `'row' \| 'column'` | 'column' | 可选,设置横向或纵向排列 | [横向排列](demo#horizontal) | | | beforeChange | `Function \| Promise \| Observable` | -- | 可选,radio-group 切换前的回调函数,返回 boolean 类型,返回 false 可以阻止 radio-group 的切换 | [回调切换](demo#condition-radio-group) | diff --git a/devui/radio/doc/api-en.md b/devui/radio/doc/api-en.md index e5dd039c..3a9865fa 100644 --- a/devui/radio/doc/api-en.md +++ b/devui/radio/doc/api-en.md @@ -20,7 +20,7 @@ In the page: | :----------------: | :----------: | :-----------------------------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | name | `string` | -- | Required. Single-option name | [Independent Radios](demo#basic-usage) | | value | `string` | -- | Required. Single-option value | [Independent Radios](demo#basic-usage) | -| disabled | `boolean` | false | Optional. Whether to disable this option. | [Disabled Radios](demo#disabled) | | +| disabled | `boolean` | false | Optional. Whether to disable this option. | [Disabled Radios](demo#disabled) | | beforeChange | `Function \| Promise \| Observable` | -- | Callback function before radio switching, which is optional. The return type is boolean. If false is returned, radio switching is prevented. | [Switch with Condition](demo#condition-change) | ## d-radio Event @@ -35,7 +35,8 @@ In the page: | Parameter | Type | Default | Description | Jump to Demo | | :----------: | :-----------------------------: | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | name | `string` | -- | Required. Single-option name (unique identifier of the radio) | [Vertical Arrangement](demo#vertical) | -| value | `array` | -- | Required. Single-choice data group | [Vertical Arrangement](demo#vertical) | +| values | `array` | -- | Required. Single-choice data group | [Vertical Arrangement](demo#vertical) | +| disabled | `boolean` | false | Optional. Whether to disable this radio-group | [Switch With Condition in A Radio Group](demo#condition-radio-group) | | cssStyle | `'row' \| 'column'` | 'column' | Optional. Set the horizontal or vertical arrangement | [Horizontal Arrangement](demo#horizontal) | | | beforeChange | `Function \| Promise \| Observable` | -- | Callback function before radio-group switching. The return value is of the boolean type. If false is returned, radio-group switching is prevented. | [Switch With Condition in A Radio Group](demo#condition-radio-group) | diff --git a/devui/read-tip/read-tip.directive.ts b/devui/read-tip/read-tip.directive.ts index e5de0877..56f46fc6 100644 --- a/devui/read-tip/read-tip.directive.ts +++ b/devui/read-tip/read-tip.directive.ts @@ -1,15 +1,17 @@ +import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostListener, + Inject, Injector, Input, OnDestroy, OnInit, TemplateRef, - ViewContainerRef, + ViewContainerRef } from '@angular/core'; import { OverlayContainerRef } from 'ng-devui/overlay-container'; import { of } from 'rxjs'; @@ -24,6 +26,7 @@ export class ReadTipDirective implements OnInit, OnDestroy { readTipComponentRef: ComponentRef; _prevTarget; + document: Document; defaultOptions: ReadTipOptions = { trigger: 'hover', @@ -105,8 +108,11 @@ export class ReadTipDirective implements OnInit, OnDestroy { private componentFactoryResolver: ComponentFactoryResolver, private overlayContainerRef: OverlayContainerRef, private inject: Injector, - private viewContainerRef: ViewContainerRef - ) {} + private viewContainerRef: ViewContainerRef, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } ngOnInit() {} @@ -124,7 +130,7 @@ export class ReadTipDirective implements OnInit, OnDestroy { this.readTipComponentRef.instance.show(); } if (rule.trigger === 'click') { - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); } } @@ -205,7 +211,7 @@ export class ReadTipDirective implements OnInit, OnDestroy { this.readTipComponentRef = null; } if (this.readTipOptions.trigger === 'click') { - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } } diff --git a/devui/select/select.component.ts b/devui/select/select.component.ts index 51d4facf..d755b647 100755 --- a/devui/select/select.component.ts +++ b/devui/select/select.component.ts @@ -3,6 +3,7 @@ import { ConnectedPosition, VerticalConnectionPos } from '@angular/cdk/overlay'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, @@ -12,6 +13,7 @@ import { ElementRef, EventEmitter, forwardRef, HostListener, + Inject, Input, NgZone, OnChanges, @@ -296,6 +298,7 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewI private filterSubscription: Subscription; public value; private resetting = false; + document: Document; private onChange = (_: any) => null; private onTouch = () => null; @@ -306,9 +309,11 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewI private i18n: I18nService, private ngZone: NgZone, private devConfigService: DevConfigService, + @Inject(DOCUMENT) private doc: any ) { this.valueParser = item => (typeof item === 'object' ? item[this.filterKey] || '' : (item + '') ? item.toString() : ''); this.formatter = item => (typeof item === 'object' ? item[this.filterKey] || '' : (item + '') ? item.toString() : ''); + this.document = this.doc; } ngOnInit(): void { @@ -356,7 +361,7 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewI if (this.i18nSubscription) { this.i18nSubscription.unsubscribe(); } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } ngOnChanges(changes: SimpleChanges): void { @@ -685,9 +690,9 @@ export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewI setDocumentClickListener() { this.ngZone.runOutsideAngular(() => { if (this.isOpen) { - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); } else { - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } }); } diff --git a/devui/shared/devui-api/devui-api.component.ts b/devui/shared/devui-api/devui-api.component.ts index b49f7680..9d47d994 100755 --- a/devui/shared/devui-api/devui-api.component.ts +++ b/devui/shared/devui-api/devui-api.component.ts @@ -1,4 +1,5 @@ -import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Component, ElementRef, Inject, Input, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { I18nService } from 'ng-devui/i18n'; import { TranslateService, TranslationChangeEvent } from '@ngx-translate/core'; @@ -16,7 +17,8 @@ export class DevUIApiComponent implements OnInit, AfterViewInit, OnDestroy { @Input() api: any; apiData: any; navSpriteInstance = null; - scrollContainer = document.documentElement; + document: Document; + scrollContainer; constructor( private router: Router, @@ -24,8 +26,12 @@ export class DevUIApiComponent implements OnInit, AfterViewInit, OnDestroy { private route: ActivatedRoute, private i18n: I18nService, private elementRef: ElementRef, - private renderer: Renderer2 - ) {} + private renderer: Renderer2, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + this.scrollContainer = this.document.documentElement; + } ngOnInit() { this.subs.add( @@ -57,7 +63,7 @@ export class DevUIApiComponent implements OnInit, AfterViewInit, OnDestroy { } refreshView() { - Array.from(document.querySelectorAll('pre code')).forEach((block) => { + Array.from(this.document.querySelectorAll('pre code')).forEach((block) => { hljs.highlightBlock(block); }); const that = this; @@ -115,6 +121,9 @@ export class DevUIApiComponent implements OnInit, AfterViewInit, OnDestroy { } get baseUrl() { + if (typeof window === 'undefined') { + return ''; + } return window.location.pathname.replace(window.location.hash, ''); } } diff --git a/devui/shared/devui-codebox/devui-codebox.component.scss b/devui/shared/devui-codebox/devui-codebox.component.scss index 3493cf39..e0d04250 100644 --- a/devui/shared/devui-codebox/devui-codebox.component.scss +++ b/devui/shared/devui-codebox/devui-codebox.component.scss @@ -24,7 +24,7 @@ .code-box-meta.markdown { position: relative; padding: 10px 40px; - border-radius: 0 0 4px 4px; + border-radius: 0 0 $devui-border-radius-feedback $devui-border-radius-feedback; transition: background-color $devui-animation-duration-slow $devui-animation-ease-in-smooth; width: 100%; font-size: $devui-font-size; @@ -83,7 +83,7 @@ .code-box .highlight-wrapper { display: none; overflow: auto; - border-radius: 0 0 4px 4px; + border-radius: 0 0 $devui-border-radius-feedback $devui-border-radius-feedback; } .code-box .highlight-wrapper-expand { diff --git a/devui/shared/devui-codebox/devui-codebox.component.ts b/devui/shared/devui-codebox/devui-codebox.component.ts index f3aa5846..9b52d9e1 100755 --- a/devui/shared/devui-codebox/devui-codebox.component.ts +++ b/devui/shared/devui-codebox/devui-codebox.component.ts @@ -1,4 +1,5 @@ -import { Component, ElementRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, ElementRef, Inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { DevuiSourceData } from './devui-source-data'; @Component({ @@ -16,6 +17,7 @@ export class DevuiCodeboxComponent implements OnInit { componentCode: Array; expanded = false; codeTabID = 'HTML'; + document: Document; copyCode(code) { this.copy(code).then(() => { @@ -32,14 +34,14 @@ export class DevuiCodeboxComponent implements OnInit { (resolve, reject): void => { let copyTextArea = null as HTMLTextAreaElement; try { - copyTextArea = document.createElement('textarea'); + copyTextArea = this.document.createElement('textarea'); copyTextArea.style.height = '0px'; copyTextArea.style.opacity = '0'; copyTextArea.style.width = '0px'; - document.body.appendChild(copyTextArea); + this.document.body.appendChild(copyTextArea); copyTextArea.value = value; copyTextArea.select(); - document.execCommand('copy'); + this.document.execCommand('copy'); resolve(value); } finally { if (copyTextArea && copyTextArea.parentNode) { @@ -57,7 +59,8 @@ export class DevuiCodeboxComponent implements OnInit { this.expanded = !this.expanded; } - constructor(private _el: ElementRef) { + constructor(private _el: ElementRef, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit() { diff --git a/devui/splitter/splitter-bar.component.html b/devui/splitter/splitter-bar.component.html index d5b6297b..45cb58b8 100644 --- a/devui/splitter/splitter-bar.component.html +++ b/devui/splitter/splitter-bar.component.html @@ -2,10 +2,13 @@ class="prev" *ngIf="showCollapseButton" dPopover - [content]="preTip" [trigger]="'hover'" [controlled]="true" [ngClass]="prevClass" + [content]="preTip" + [showAnimate]="false" + [mouseEnterDelay]="100" + [mouseLeaveDelay]="0" (click)="collapsePrePane()" (touchstart)="collapsePrePane()" >
@@ -14,10 +17,10 @@ class="next" *ngIf="showCollapseButton" dPopover - [content]="nextTip" [trigger]="'hover'" [controlled]="true" [ngClass]="nextClass" + [content]="nextTip" (click)="collapseNextPane()" (touchstart)="collapseNextPane()" > diff --git a/devui/splitter/splitter-bar.component.ts b/devui/splitter/splitter-bar.component.ts index 187ccd64..6b907fa2 100644 --- a/devui/splitter/splitter-bar.component.ts +++ b/devui/splitter/splitter-bar.component.ts @@ -68,7 +68,7 @@ export class SplitterBarComponent implements OnInit, AfterViewInit, OnDestroy { pageX, pageY })) - ) + ) constructor(private el: ElementRef, private splitter: SplitterService, @@ -132,12 +132,12 @@ export class SplitterBarComponent implements OnInit, AfterViewInit, OnDestroy { queryPanes(index, nearIndex) { const pane = this.splitter.getPane(index); const nearPane = this.splitter.getPane(nearIndex); - return {pane, nearPane}; + return { pane, nearPane }; } // 切换是否允许拖拽,收起时不能拖拽 toggleResize() { - const {pane, nearPane} = this.queryPanes(this.index, this.index + 1); + const { pane, nearPane } = this.queryPanes(this.index, this.index + 1); const isCollapsed = pane.collapsed || nearPane.collapsed; if (isCollapsed) { this.renderer.addClass(this.el.nativeElement, 'none-resizable'); @@ -148,8 +148,8 @@ export class SplitterBarComponent implements OnInit, AfterViewInit, OnDestroy { // 计算前面板收起操作样式 get prevClass() { - const {pane, nearPane} = this.queryPanes(this.index, this.index + 1); - this.preTip = pane.collapsed ? this.splitterText.collapse : this.splitterText.expand; + const { pane, nearPane } = this.queryPanes(this.index, this.index + 1); + this.preTip = pane.collapsed ? this.splitterText.expand : this.splitterText.collapse; // 第一个面板或者其它面板折叠方向不是向后的显示操作按钮 const showIcon = (pane.collapseDirection !== 'after' || this.index === 0); return this.generateCollapseClass(pane, nearPane, showIcon); @@ -157,8 +157,8 @@ export class SplitterBarComponent implements OnInit, AfterViewInit, OnDestroy { // 计算相邻面板收起操作样式 get nextClass() { - const {pane, nearPane} = this.queryPanes(this.index + 1, this.index); - this.nextTip = pane.collapsed ? this.splitterText.collapse : this.splitterText.expand; + const { pane, nearPane } = this.queryPanes(this.index + 1, this.index); + this.nextTip = pane.collapsed ? this.splitterText.expand : this.splitterText.collapse; // 最后一个面板或者其它面板折叠方向不是向前的显示操作按钮 const showIcon = (pane.collapseDirection !== 'before' || this.index + 1 === this.splitter.paneCount - 1); return this.generateCollapseClass(pane, nearPane, showIcon); @@ -171,7 +171,7 @@ export class SplitterBarComponent implements OnInit, AfterViewInit, OnDestroy { // 根据当前状态生成收起按钮样式 generateCollapseClass(pane, nearPane, showIcon) { - // 是否允许收起 + // 是否允许收起 const isCollapsible = pane.collapsible && showIcon; // 当前收起状态 const isCollapsed = pane.collapsed; diff --git a/devui/steps-guide/demo/basic/basic.component.html b/devui/steps-guide/demo/basic/basic.component.html index 2f2e907e..8aa64bb4 100644 --- a/devui/steps-guide/demo/basic/basic.component.html +++ b/devui/steps-guide/demo/basic/basic.component.html @@ -17,6 +17,7 @@ [steps]="steps" [stepIndex]="1" [dStepsGuidePosition]="'top'" + [beforeChange]="beforeChange" (operateChange)="operateChange($event)" (click)="reset(1)" > diff --git a/devui/steps-guide/demo/basic/basic.component.ts b/devui/steps-guide/demo/basic/basic.component.ts index c7016824..54f18026 100644 --- a/devui/steps-guide/demo/basic/basic.component.ts +++ b/devui/steps-guide/demo/basic/basic.component.ts @@ -16,11 +16,11 @@ export class BasicComponent implements OnInit { ngOnInit() { this.stepService.currentIndex.subscribe((index) => (this.currentStep = index)); /* 由于整个demo是在一个页面内显示多个操作指引序列,因此需要在初始化时重置显示状态 */ - localStorage.setItem('devui_guide_step-position-demo', '0'); + localStorage.setItem('devui_guide_step-position-demo', '0'); /* 设置第三个序列为不显示状态 */ localStorage.setItem('devui_guide_step-custom-demo', '0'); /* 设置第二个序列为不显示状态 */ - localStorage.removeItem('devui_guide_step-basic-demo'); /* 设置第一个序列为显示状态 */ this.stepService.setSteps(this.steps); /* 将步骤数据设置为第一个序列的内容 */ this.stepService.setCurrentIndex(0); /* 设置当前序列显示步骤为第一个步骤 */ + this.stepService.showGuide(true); /* 显示当前序列 */ } reset(index = 0) { @@ -36,6 +36,14 @@ export class BasicComponent implements OnInit { this.stepService.showGuide(false); } + beforeChange = (currentIndex, targetIndex) => { + /* 当前步骤为第三步,切换至第二步时,阻止步骤显示,显示第一步 */ + if (currentIndex === 2 && targetIndex === 1) { + this.stepService.setCurrentIndex(0); + return false; + } + } + operateChange(response) { this.currentStepOutPut = response; if (response.clickType === 'close' && response.currentIndex === 2) { diff --git a/devui/steps-guide/demo/custom/custom.component.ts b/devui/steps-guide/demo/custom/custom.component.ts index 29b8e00a..d186051e 100644 --- a/devui/steps-guide/demo/custom/custom.component.ts +++ b/devui/steps-guide/demo/custom/custom.component.ts @@ -1,10 +1,10 @@ +import { DOCUMENT } from '@angular/common'; import { Component, + Inject, OnInit } from '@angular/core'; - import { StepsGuideService } from 'ng-devui/steps-guide'; - import { customData } from '../fakeData'; @Component({ @@ -21,13 +21,13 @@ export class CustomComponent implements OnInit { hideStepNav: true, }; - constructor(private stepService: StepsGuideService) {} + constructor(private stepService: StepsGuideService, @Inject(DOCUMENT) private doc: any) {} ngOnInit() { - this.targetElement = document.querySelector('.header-menu>div:first-child'); + this.targetElement = this.doc.querySelector('.header-menu>div:first-child'); /* 用于判断内容变化的dom,可选择最接近变化范围的容器, 在angular组件或路由下可能不能正确触发,可选择更外层容器尝试 */ - this.observerDom = document.querySelector('.header-menu'); + this.observerDom = this.doc.querySelector('.header-menu'); } reset(index = 0) { diff --git a/devui/steps-guide/doc/api-cn.md b/devui/steps-guide/doc/api-cn.md index 6fbb4539..1d803028 100644 --- a/devui/steps-guide/doc/api-cn.md +++ b/devui/steps-guide/doc/api-cn.md @@ -17,21 +17,23 @@ import { StepsGuideModule } from 'ng-devui/steps-guide'; ## dStepsGuide 参数 -| 参数 | 类型 | 默认 | 说明 | 跳转 Demo |全局配置项| -| :----------------: | :------------------: | :-------------------------------------------------: | :--: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -| pageName | `string` | -- | 必选,用于标识操作指引是否显示,一组操作指引序列建议使用相同值 | [基本用法](demo#basic-usage) | -| steps | `Array<`[`StepItem`](#stepitem)`>` | [] | 必选,操作指引步骤数组,如通过 StepsGuideService.setSteps 设置了操作指引步骤,则优先使用服务中的,StepItem 对象定义见下 | [基本用法](demo#basic-usage) | -| stepIndex | `number` | -- | 必选,当前步骤在整个操作指引序列中的索引 | [基本用法](demo#basic-usage) | -| ~~position~~ | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | 可选,指引信息弹出的位置方向,可选值:top、top-left、top-right、bottom、bottom-left、bottom-right、left、right(`已废弃,请使用dStepsGuidePosition`) | [基本用法](demo#basic-usage) | -| dStepsGuidePosition | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | 可选,指引信息弹出的位置方向,可选值:top、top-left、top-right、bottom、bottom-left、bottom-right、left、right | [基本用法](demo#basic-usage) | -| leftFix | `number` | 0 | 可选,用于修正指引信息的位置 | [自定义位置](demo#custom-usage) | -| topFix | `number` | 0 | 可选,用于修正指引信息的位置 | [自定义位置](demo#custom-usage) | -| zIndex | `number` | 1100 | 可选,用于调整指引信息的显示层级 | [自定义位置](demo#custom-usage) | -| targetElement | `HTMLElement` | -- | 可选,指引信息显示的目标 dom ,如果指定,不再使用指令所在的 dom 作为目标 | [自定义位置](demo#custom-usage) | -| scrollElement | `HTMLElement` | -- | 可选,指引信息跟随滚动定位的容器 dom ,默认会自动获取,如果与预想 dom 不同时需要指定 | | -| scrollToTargetSwitch | `boolean` | true | 可选,是否自动滚动页面至指引信息显示的位置 dom | [基本用法](demo#basic-usage) | -| observerDom | `HTMLElement` | -- | 可选,允许用户指定一个 dom 反馈页面变化。主要用于用户无法控制或判断的且不会触发 resize 事件的 dom 改变导致指引信息位置变化的情况,例如:指引信息绑定在 fixed 定位的头部菜单,页面随路由跳转内容变化会显示或隐藏滚动条导致头部菜单的 dom 位置发生变化 | [自定义位置](demo#custom-usage) | -| extraConfig | [`ExtraConfig`](#extraconfig) | -- | 可选,扩展配置,用于隐藏上一步按钮和步骤圆点图标,ExtraConfig 对象定义见下文 | [自定义位置](demo#custom-usage) | +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| :------------------: | :-------------------------------------------------: | :--: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------ | ---------- | +| pageName | `string` | -- | 必选,用于标识操作指引是否显示,一组操作指引序列建议使用相同值 | [基本用法](demo#basic-usage) | +| steps | `Array<`[`StepItem`](#stepitem)`>` | [] | 必选,操作指引步骤数组,如通过 StepsGuideService.setSteps 设置了操作指引步骤,则优先使用服务中的,StepItem 对象定义见下 | [基本用法](demo#basic-usage) | +| stepIndex | `number` | -- | 必选,当前步骤在整个操作指引序列中的索引 | [基本用法](demo#basic-usage) | +| beforeChange | `Function\|Promise\|Observable` | -- | 可选,在切换步骤时前置执行,返回 boolean 值决定是否显示当前步骤 | [基本用法](demo#basic-usage) | +| ~~position~~ | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | 可选,指引信息弹出的位置方向,可选值:top、top-left、top-right、bottom、bottom-left、bottom-right、left、right(`已废弃,请使用dStepsGuidePosition`) | [基本用法](demo#basic-usage) | +| dStepsGuidePosition | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | 可选,指引信息弹出的位置方向,可选值:top、top-left、top-right、bottom、bottom-left、bottom-right、left、right | [基本用法](demo#basic-usage) | +| leftFix | `number` | 0 | 可选,用于修正指引信息的位置 | [自定义位置](demo#custom-usage) | +| topFix | `number` | 0 | 可选,用于修正指引信息的位置 | [自定义位置](demo#custom-usage) | +| zIndex | `number` | 1100 | 可选,用于调整指引信息的显示层级 | [自定义位置](demo#custom-usage) | +| targetElement | `HTMLElement` | -- | 可选,指引信息显示的目标 dom ,如果指定,不再使用指令所在的 dom 作为目标 | [自定义位置](demo#custom-usage) | +| scrollElement | `HTMLElement` | -- | 可选,指引信息跟随滚动定位的容器 dom ,默认会自动获取,如果与预想 dom 不同时需要指定 | | +| scrollToTargetSwitch | `boolean` | true | 可选,是否自动滚动页面至指引信息显示的位置 dom | [基本用法](demo#basic-usage) | + +| observerDom | `HTMLElement` | -- | 可选,允许用户指定一个 dom 反馈页面变化。主要用于用户无法控制或判断的且不会触发 resize 事件的 dom 改变导致指引信息位置变化的情况,例如:指引信息绑定在 fixed 定位的头部菜单,页面随路由跳转内容变化会显示或隐藏滚动条导致头部菜单的 dom 位置发生变化 | [自定义位置](demo#custom-usage) | +| extraConfig | [`ExtraConfig`](#extraconfig) | -- | 可选,扩展配置,用于隐藏上一步按钮和步骤圆点图标,ExtraConfig 对象定义见下文 | [自定义位置](demo#custom-usage) | ### StepItem diff --git a/devui/steps-guide/doc/api-en.md b/devui/steps-guide/doc/api-en.md index 955fc1d5..7fea01fe 100644 --- a/devui/steps-guide/doc/api-en.md +++ b/devui/steps-guide/doc/api-en.md @@ -17,22 +17,24 @@ In the page ## dStepsGuide Parameter -| Parameter | Type | Default | Description | Jump to Demo |Global Config| -| :----------------: | :------------------: | :-------------------------------------------------: | :-----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- | -| pageName | `string` | -- | Required. This parameter specifies whether to display the operation guide. It is recommended that you use the same value for a group of operation guide sequences. | [Basic usage](demo#basic-usage) | -| steps | `Array<`[`StepItem`](#stepitem)`>` | [] | Required. Operation guide step array. If an operation guide step is set through StepsGuideService.setSteps, the step in the service is preferentially used. For the definition of the StepItem object, see the following | [Basic usage](demo#basic-usage) | -| stepIndex | `number` | -- | Required. Index of the current step in the operation guide sequence. | [Basic usage](demo#basic-usage) | -| ~~position~~ | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | Optional. Guide the position and direction of information dragging, optional values: top, top-left, top-right, bottom, left-left, bottom-right, left, right (`deprecated, please use dStepsGuidePosition`). | [Basic usage](demo#basic-usage) | -| dStepsGuidePosition | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | Optional. Guide the position and direction of the information pop-up, optional values: top, top-left, top-right, bottom, bottom-left, bottom-right, left, right | [Basic usage](demo#basic-usage) | -| leftFix | `number` | 0 | Optional. It is used to correct the location of the guidance information. | [Customized location](demo#custom-usage) | -| topFix | `number` | 0 | Optional. Used to correct the location of guidance information. | [Customized location](demo#custom-usage) | -| zIndex | `number` | 1100 | Optional. This parameter is used to adjust the display level of guidance information. | [Customized position](demo#custom-usage) | -| targetElement | `HTMLElement` | -- | Optional. This parameter specifies the target dom in the instruction information. If this parameter is specified, the dom where the instruction is located is not used as the target. | [Customized location](demo#custom-usage) | -| scrollToTargetSwitch | `boolean` | true | Optional. Indicates whether to automatically scroll to the location where the guide information is displayed. dom | [Customized location](demo#custom-usage) | -| targetElement | `HTMLElement` | -- | Optional. This parameter specifies the target dom in the instruction information. If this parameter is specified, the dom where the instruction is located is not used as the target. | [Customized location](demo#custom-usage) | -| scrollToTargetSwitch | `boolean` | true | Optional. Indicates whether to automatically scroll to the location where the guide information is displayed. dom | [Customized location](demo#custom-usage) | -| observerDom | `HTMLElement` | -- | Optional. Allows users to specify a dom to report page changes. This parameter is used when the location of the guide information changes due to the dom change that cannot be controlled or determined by the user and does not trigger the resize event. For example, the guide information is bound to the header menu of fixed positioning, the dom position of the header menu changes because the scroll bar is displayed or hidden when the page changes with the route. | [Customized position](demo#custom-usage) | -| extraConfig | [`ExtraConfig`](#extraconfig) | -- | Optional. Extended configuration used to hide the button and dot icon of the previous step. For details about the definition of the ExtraConfig object, see the following description. | [Customized location](demo#custom-usage) | +| Parameter | Type | Default | Description | Jump to Demo | Global Config | +| :------------------: | :-------------------------------------------------: | :-----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------- | ------------- | +| pageName | `string` | -- | Required. This parameter specifies whether to display the operation guide. It is recommended that you use the same value for a group of operation guide sequences. | [Basic usage](demo#basic-usage) | +| steps | `Array<`[`StepItem`](#stepitem)`>` | [] | Required. Operation guide step array. If an operation guide step is set through StepsGuideService.setSteps, the step in the service is preferentially used. For the definition of the StepItem object, see the following | [Basic usage](demo#basic-usage) | +| stepIndex | `number` | -- | Required. Index of the current step in the operation guide sequence. | [Basic usage](demo#basic-usage) | +| beforeChange | `Function\|Promise\|Observable` | -- | Optional. This function is executed before the step switchover. The boolean value is returned to determine whether to display this step. | [基本用法](demo#basic-usage) | +| ~~position~~ | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | Optional. Guide the position and direction of information dragging, optional values: top, top-left, top-right, bottom, left-left, bottom-right, left, right (`deprecated, please use dStepsGuidePosition`). | [Basic usage](demo#basic-usage) | +| dStepsGuidePosition | [`StepsGuidePositionType`](#stepsguidepositiontype) | top | Optional. Guide the position and direction of the information pop-up, optional values: top, top-left, top-right, bottom, bottom-left, bottom-right, left, right | [Basic usage](demo#basic-usage) | +| leftFix | `number` | 0 | Optional. It is used to correct the location of the guidance information. | [Customized location](demo#custom-usage) | +| topFix | `number` | 0 | Optional. Used to correct the location of guidance information. | [Customized location](demo#custom-usage) | +| zIndex | `number` | 1100 | Optional. This parameter is used to adjust the display level of guidance information. | [Customized position](demo#custom-usage) | +| targetElement | `HTMLElement` | -- | Optional. This parameter specifies the target dom in the instruction information. If this parameter is specified, the dom where the instruction is located is not used as the target. | [Customized location](demo#custom-usage) | +| scrollToTargetSwitch | `boolean` | true | Optional. Indicates whether to automatically scroll to the location where the guide information is displayed. dom | [Customized location](demo#custom-usage) | +| targetElement | `HTMLElement` | -- | Optional. This parameter specifies the target dom in the instruction information. If this parameter is specified, the dom where the instruction is located is not used as the target. | [Customized location](demo#custom-usage) | + +| scrollToTargetSwitch | `boolean` | true | Optional. Indicates whether to automatically scroll to the location where the guide information is displayed. dom | [Customized location](demo#custom-usage) | +| observerDom | `HTMLElement` | -- | Optional. Allows users to specify a dom to report page changes. This parameter is used when the location of the guide information changes due to the dom change that cannot be controlled or determined by the user and does not trigger the resize event. For example, the guide information is bound to the header menu of fixed positioning, the dom position of the header menu changes because the scroll bar is displayed or hidden when the page changes with the route. | [Customized position](demo#custom-usage) | +| extraConfig | [`ExtraConfig`](#extraconfig) | -- | Optional. Extended configuration used to hide the button and dot icon of the previous step. For details about the definition of the ExtraConfig object, see the following description. | [Customized location](demo#custom-usage) | ### StepItem diff --git a/devui/steps-guide/steps-guide.component.scss b/devui/steps-guide/steps-guide.component.scss index 3ab5272b..a806dfe2 100644 --- a/devui/steps-guide/steps-guide.component.scss +++ b/devui/steps-guide/steps-guide.component.scss @@ -21,10 +21,6 @@ padding: 0; } - .devui-content { - opacity: 0.8; - } - > .devui-arrow, > .devui-arrow::after { position: absolute; diff --git a/devui/steps-guide/steps-guide.component.ts b/devui/steps-guide/steps-guide.component.ts index 2e9284ee..495ca6dd 100644 --- a/devui/steps-guide/steps-guide.component.ts +++ b/devui/steps-guide/steps-guide.component.ts @@ -1,4 +1,5 @@ -import { AfterViewInit, Component, ElementRef, HostBinding, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Component, ElementRef, HostBinding, Inject, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { I18nInterface, I18nService } from 'ng-devui/i18n'; import { PositionService } from 'ng-devui/position'; import { fromEvent, Subscription } from 'rxjs'; @@ -38,14 +39,18 @@ export class StepsGuideComponent implements OnInit, AfterViewInit, OnDestroy { i18nCommonText: I18nInterface['stepsGuide']; DOT_HORIZONTAL_MARGIN = 27; DOT_VERTICAL_MARGIN = 22; + document: Document; constructor( private stepService: StepsGuideService, private renderer: Renderer2, private positionService: PositionService, private elm: ElementRef, - private i18n: I18nService - ) {} + private i18n: I18nService, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } ngOnInit() { this.dots = new Array(this.stepsCount); @@ -67,7 +72,7 @@ export class StepsGuideComponent implements OnInit, AfterViewInit, OnDestroy { this.updatePosition(); if (!this.scrollElement) { const currentScrollElement = this.positionService.getScrollParent(this.triggerElement); - this.scrollElement = currentScrollElement === document.body ? window : currentScrollElement; + this.scrollElement = currentScrollElement === this.document.body ? window : currentScrollElement; } const scrollSubscriber = fromEvent(this.scrollElement, 'scroll') .pipe(throttleTime(this.SCROLL_REFRESH_INTERVAL, undefined, { leading: true, trailing: true })) @@ -112,10 +117,10 @@ export class StepsGuideComponent implements OnInit, AfterViewInit, OnDestroy { switch (rect.placementSecondary) { case 'left': - left = targetRect.left + this.triggerElement.clientWidth / 2 - this.DOT_VERTICAL_MARGIN; + left = targetRect.left; break; case 'right': - left = targetRect.left + this.triggerElement.clientWidth / 2 - this.elm.nativeElement.clientWidth + this.DOT_VERTICAL_MARGIN; + left = targetRect.left - this.elm.nativeElement.clientWidth + this.triggerElement.clientWidth; break; default: } diff --git a/devui/steps-guide/steps-guide.directive.ts b/devui/steps-guide/steps-guide.directive.ts index e32f9443..9a4e59fc 100644 --- a/devui/steps-guide/steps-guide.directive.ts +++ b/devui/steps-guide/steps-guide.directive.ts @@ -1,9 +1,11 @@ +import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, + Inject, Input, OnDestroy, OnInit, @@ -11,7 +13,7 @@ import { } from '@angular/core'; import { OverlayContainerRef } from 'ng-devui/overlay-container'; import { throttle } from 'lodash-es'; -import { Subscription } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { StepsGuideComponent } from './steps-guide.component'; import { StepsGuideService } from './steps-guide.service'; import { @@ -23,7 +25,7 @@ import { } from './steps-guide.types'; @Directive({ - selector: '[dStepsGuide]' + selector: '[dStepsGuide]', }) export class StepsGuideDirective implements OnInit, OnDestroy { // 引导页面标示,用于记录不同页面的引导状态 @@ -77,6 +79,7 @@ export class StepsGuideDirective implements OnInit, OnDestroy { this.destroyMutationObserver(true); } } + @Input() beforeChange: (currentIndex, targetIndex) => boolean | Promise | Observable; // 点击引导操作触发,返回当前步骤和当前操作,比如上一步、下一步、关闭 @Output() operateChange = new EventEmitter(); _observerDom: any; @@ -88,17 +91,26 @@ export class StepsGuideDirective implements OnInit, OnDestroy { // 监听dom变化的设置,监听属性变化和dom所属节点树变化 MUTATION_OBSERVER_CONFIG = { attributes: true, subtree: true }; MUTATION_OBSERVER_TIME = 500; + document: Document; constructor( private stepService: StepsGuideService, private elm: ElementRef, private componentFactoryResolver: ComponentFactoryResolver, - private overlayContainerRef: OverlayContainerRef - ) { } + private overlayContainerRef: OverlayContainerRef, + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } ngOnInit() { // 监听当前索引变化,决定显示步骤 this.sub.add(this.stepService.currentIndex.subscribe(index => { + this.canChange(index).then((change) => { + if (!change) { + return; + } + }); // 防止服务中的步骤被置空或默认为空 const serviceSteps = this.stepService.getSteps() || []; this.steps = serviceSteps.length > 0 ? serviceSteps : this.steps; @@ -129,7 +141,7 @@ export class StepsGuideDirective implements OnInit, OnDestroy { position: this.dStepsGuidePosition, leftFix: this.leftFix, topFix: this.topFix, - zIndex: this.zIndex + zIndex: this.zIndex, }); }); } @@ -161,7 +173,7 @@ export class StepsGuideDirective implements OnInit, OnDestroy { } insert(option: GuideOptions) { - const bodyDoms = Array.from(document.body.childNodes); + const bodyDoms = Array.from(this.document.body.childNodes); const hasGuide = bodyDoms && bodyDoms.find((dom: HTMLElement) => dom.className && dom.className.indexOf('devui-step-item') >= 0); if (!hasGuide) { this.stepRef = this.overlayContainerRef.createComponent( @@ -176,8 +188,11 @@ export class StepsGuideDirective implements OnInit, OnDestroy { } // 监听dom变化的回调方法,即dom发生变化时触发resize事件 - mutationCallBack() { - const resizeEvt = document.createEvent('Event'); + mutationCallBack = () => { + if (typeof window === 'undefined') { + return; + } + const resizeEvt = this.document.createEvent('Event'); resizeEvt.initEvent('resize', true, true); window.dispatchEvent(resizeEvt); } @@ -192,4 +207,22 @@ export class StepsGuideDirective implements OnInit, OnDestroy { this.observer = undefined; } } + + canChange(index) { + let changeResult = Promise.resolve(true); + const currentIndex = this.currentIndex >= 0 ? this.currentIndex : this.stepIndex; + if (this.beforeChange) { + const result: any = this.beforeChange(currentIndex, index); + if (typeof result !== 'undefined') { + if (result.then) { + changeResult = result; + } else if (result.subscribe) { + changeResult = (result as Observable).toPromise(); + } else { + changeResult = Promise.resolve(result); + } + } + } + return changeResult; + } } diff --git a/devui/sticky/demo/basic/basic.component.html b/devui/sticky/demo/basic/basic.component.html index d8abb82c..2073ad0d 100755 --- a/devui/sticky/demo/basic/basic.component.html +++ b/devui/sticky/demo/basic/basic.component.html @@ -6,7 +6,7 @@
-

Basic Info

+

Basic Info

Display basic info here
@@ -18,7 +18,7 @@

Tips:

-

Issue List

+

Issue List

Display issue list here
diff --git a/devui/sticky/demo/basic/basic.component.ts b/devui/sticky/demo/basic/basic.component.ts index cde49278..aa2cd374 100755 --- a/devui/sticky/demo/basic/basic.component.ts +++ b/devui/sticky/demo/basic/basic.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, OnInit } from '@angular/core'; @Component({ selector: 'd-basic', @@ -11,10 +12,10 @@ export class BasicComponent implements OnInit { bottom: 0 }; demoDocViewerMain; - constructor() { } + constructor(@Inject(DOCUMENT) private doc: any) { } ngOnInit() { - this.demoDocViewerMain = document.querySelector('.doc-viewer-container .main'); + this.demoDocViewerMain = this.doc.querySelector('.doc-viewer-container .main'); } log(...v) { console.log(...v); diff --git a/devui/sticky/demo/scroll-target/scroll-target.component.html b/devui/sticky/demo/scroll-target/scroll-target.component.html index 23bfce7c..e1d9742a 100644 --- a/devui/sticky/demo/scroll-target/scroll-target.component.html +++ b/devui/sticky/demo/scroll-target/scroll-target.component.html @@ -6,7 +6,7 @@
-

Basic Info

+

Basic Info

Display basic info here
@@ -16,7 +16,7 @@

Tips:

-

Issue List

+

Issue List

Display issue list here
diff --git a/devui/sticky/demo/scroll-target/scroll-target.component.ts b/devui/sticky/demo/scroll-target/scroll-target.component.ts index f59b983e..d5db1c1d 100644 --- a/devui/sticky/demo/scroll-target/scroll-target.component.ts +++ b/devui/sticky/demo/scroll-target/scroll-target.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, OnInit } from '@angular/core'; @Component({ selector: 'd-scroll-target', @@ -11,10 +12,10 @@ export class ScrollTargetComponent implements OnInit { bottom: 0 }; demoDocViewerMain; - constructor() { } + constructor(@Inject(DOCUMENT) private doc: any) { } ngOnInit() { - this.demoDocViewerMain = document.querySelector('.doc-viewer-container .main'); + this.demoDocViewerMain = this.doc.querySelector('.doc-viewer-container .main'); } } diff --git a/devui/sticky/sticky.component.ts b/devui/sticky/sticky.component.ts index d89275bf..0e90b2f6 100755 --- a/devui/sticky/sticky.component.ts +++ b/devui/sticky/sticky.component.ts @@ -9,8 +9,9 @@ import { OnDestroy, OnInit, Output, - ViewChild, + ViewChild } from '@angular/core'; +import { WindowRef } from 'ng-devui/window-ref'; import { fromEvent, Subscription } from 'rxjs'; import { filter, throttleTime } from 'rxjs/operators'; @@ -61,7 +62,7 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { private scrollTimer; subscription: Subscription; - constructor(private el: ElementRef) {} + constructor(private el: ElementRef, private windowRef: WindowRef) {} ngOnInit() { this.parentNode = this.el.nativeElement.parentNode; @@ -71,18 +72,18 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { } ngAfterViewInit() { - this.scrollTarget = this.scrollTarget || window; // window有scroll事件,document.documentElement没有scroll事件 + this.scrollTarget = this.scrollTarget || this.windowRef.window; // window有scroll事件,document.documentElement没有scroll事件 this.scrollTarget.addEventListener('scroll', this.throttle); this.initScrollStatus(this.scrollTarget); - if (this.scrollTarget !== window) { - this.subscription = fromEvent(window, 'scroll') + if (this.scrollTarget !== this.windowRef.window) { + this.subscription = fromEvent(this.windowRef.window, 'scroll') .pipe( throttleTime(100, undefined, { leading: true, trailing: true }), filter( (event) => event.target !== this.scrollTarget && - (event.target === window || - event.target === document || // fix ie11 document.contains is not defined + (event.target === this.windowRef.window || + event.target === this.windowRef.document || // fix ie11 document.contains is not defined ((event.target).contains && (event.target).contains(this.scrollTarget))) ) ) @@ -107,7 +108,8 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { this.wrapper.nativeElement.style.position = 'static'; break; case 'follow': - const viewOffset = this.scrollTarget && this.scrollTarget !== window ? this.scrollTarget.getBoundingClientRect().top : 0; + const viewOffset = this.scrollTarget && this.scrollTarget !== this.windowRef.window ? + this.scrollTarget.getBoundingClientRect().top : 0; this.wrapper.nativeElement.style.top = +viewOffset + ((this.view && this.view.top) || 0) + 'px'; this.wrapper.nativeElement.style.left = this.wrapper.nativeElement.getBoundingClientRect().left + 'px'; this.wrapper.nativeElement.style.position = 'fixed'; @@ -165,8 +167,9 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { } scrollHandler = () => { - const viewOffsetTop = this.scrollTarget && this.scrollTarget !== window ? this.scrollTarget.getBoundingClientRect().top : 0; - const computedStyle = window.getComputedStyle(this.container); + const viewOffsetTop = this.scrollTarget && this.scrollTarget !== this.windowRef.window ? + this.scrollTarget.getBoundingClientRect().top : 0; + const computedStyle = this.windowRef.window.getComputedStyle(this.container); if (this.parentNode.getBoundingClientRect().top - viewOffsetTop > ((this.view && this.view.top) || 0)) { this.status = 'normal'; // 全局滑动(container!==parentNode)时候增加预判 } else if ( @@ -200,8 +203,8 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { left: ['left', 'Left'], top: ['top', 'Top'], }; - if (window && window.getComputedStyle) { - const computedStyle = window.getComputedStyle(relativeElement); + if (this.windowRef.window && this.windowRef.window.getComputedStyle) { + const computedStyle = this.windowRef.window.getComputedStyle(relativeElement); return ( element.getBoundingClientRect()[key[direction][0]] - relativeElement.getBoundingClientRect()[key[direction][0]] - @@ -211,8 +214,8 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { } } calculateRemainPosition(element, relativeElement, container) { - if (window && window.getComputedStyle) { - const computedStyle = window.getComputedStyle(container); + if (this.windowRef.window && this.windowRef.window.getComputedStyle) { + const computedStyle = this.windowRef.window.getComputedStyle(container); const result = container.getBoundingClientRect().height - element.getBoundingClientRect().height + @@ -227,7 +230,8 @@ export class StickyComponent implements OnInit, AfterViewInit, OnDestroy { } initScrollStatus(target) { - const scrollTargets = target === window ? [document.documentElement, document.body] : [target]; + const scrollTargets = target === this.windowRef.window ? + [this.windowRef.document.documentElement, this.windowRef.document.body] : [target]; let flag = false; scrollTargets.forEach((scrollTarget) => { if (scrollTarget.scrollTop && scrollTarget.scrollTop > 0) { diff --git a/devui/sticky/sticky.module.ts b/devui/sticky/sticky.module.ts index 347db148..4fc175e2 100755 --- a/devui/sticky/sticky.module.ts +++ b/devui/sticky/sticky.module.ts @@ -1,9 +1,10 @@ import { NgModule } from '@angular/core'; +import { WindowRefModule } from 'ng-devui/window-ref'; import { StickyComponent } from './sticky.component'; @NgModule({ imports: [ - + WindowRefModule ], declarations: [ StickyComponent diff --git a/devui/style/font/Oswald-VariableFont_wght.ttf b/devui/style/font/Oswald-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7601ddd2747739757a6f9b71c482b44df3cf9a5d GIT binary patch literal 153152 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≻F!x??*eJH7ldHK*_?gd1h?ao|0G%dZW_-LDXo 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 Nb3r;?CY?_y`5RHLb^`za literal 0 HcmV?d00001 diff --git a/devui/tabs/demo/tabs-demo.component.html b/devui/tabs/demo/tabs-demo.component.html index b4c8013d..8600764f 100755 --- a/devui/tabs/demo/tabs-demo.component.html +++ b/devui/tabs/demo/tabs-demo.component.html @@ -19,13 +19,13 @@

-
+
{{ 'components.tabs.typeSliderDemo.title' | translate }}
-
+
{{ 'components.tabs.typeWrappedDemo.title' | translate }}
diff --git a/devui/tabs/demo/tabs-demo.component.ts b/devui/tabs/demo/tabs-demo.component.ts index 5e69b804..64c487c0 100755 --- a/devui/tabs/demo/tabs-demo.component.ts +++ b/devui/tabs/demo/tabs-demo.component.ts @@ -1,4 +1,4 @@ -import { Component, isDevMode, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { DevuiSourceData } from 'ng-devui/shared/devui-codebox'; import { TranslateService, TranslationChangeEvent } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; @@ -57,7 +57,6 @@ export class TabsDemoComponent implements OnInit, OnDestroy { code: require('!!raw-loader!./configurable-tabs/tabs-transfer/tabs-transfer.component.ts'), }, ]; - devMode = isDevMode(); navItems = []; subs: Subscription = new Subscription(); constructor(private translate: TranslateService) { } diff --git a/devui/textarea/demo/count/count.component.html b/devui/textarea/demo/count/count.component.html new file mode 100644 index 00000000..3e40d653 --- /dev/null +++ b/devui/textarea/demo/count/count.component.html @@ -0,0 +1,4 @@ + + +
{{ curLength }}/{{ maxLength }}
+
diff --git a/devui/textarea/demo/count/count.component.scss b/devui/textarea/demo/count/count.component.scss new file mode 100644 index 00000000..d29c1717 --- /dev/null +++ b/devui/textarea/demo/count/count.component.scss @@ -0,0 +1,3 @@ +.count-length { + float: right; +} diff --git a/devui/textarea/demo/count/count.component.ts b/devui/textarea/demo/count/count.component.ts new file mode 100644 index 00000000..e0b47e76 --- /dev/null +++ b/devui/textarea/demo/count/count.component.ts @@ -0,0 +1,25 @@ +import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'; + +@Component({ + selector: 'd-count', + templateUrl: './count.component.html', + styleUrls: ['./count.component.scss'] +}) +export class CountComponent implements OnInit { + @ViewChild('countArea') countArea: ElementRef; + curLength: number; + maxLength: number; + + constructor() { } + + ngOnInit() { + setTimeout(() => { + this.curLength = 0; + this.maxLength = this.countArea.nativeElement.maxLength; + }); + } + + @HostListener('input') currentLength() { + this.curLength = this.countArea.nativeElement.value.length; + } +} diff --git a/devui/textarea/demo/text-demo.component.html b/devui/textarea/demo/text-demo.component.html index b2247f1a..307b2c82 100644 --- a/devui/textarea/demo/text-demo.component.html +++ b/devui/textarea/demo/text-demo.component.html @@ -14,4 +14,11 @@
+
+
{{ 'components.textarea.countDemo.title' | translate }}
+
+ + + +
diff --git a/devui/textarea/demo/text-demo.component.ts b/devui/textarea/demo/text-demo.component.ts index 4809bd7b..b1114ba6 100644 --- a/devui/textarea/demo/text-demo.component.ts +++ b/devui/textarea/demo/text-demo.component.ts @@ -17,6 +17,11 @@ export class TextDemoComponent implements OnInit, OnDestroy { { title: 'TS', language: 'typescript', code: require('!!raw-loader!./resize/resize.component.ts') }, { title: 'SCSS', language: 'css', code: require('!!raw-loader!./resize/resize.component.css') }, ]; + countSource: Array = [ + { title: 'HTML', language: 'xml', code: require('!!raw-loader!./count/count.component.html') }, + { title: 'TS', language: 'typescript', code: require('!!raw-loader!./count/count.component.ts') }, + { title: 'SCSS', language: 'css', code: require('!!raw-loader!./count/count.component.scss') }, + ]; navItems = []; subs: Subscription = new Subscription(); @@ -41,6 +46,7 @@ export class TextDemoComponent implements OnInit, OnDestroy { this.navItems = [ { dAnchorLink: 'basic-usage', value: values['basic-usage'] }, { dAnchorLink: 'resize', value: values['resize'] }, + { dAnchorLink: 'count', value: values['count'] }, ]; } diff --git a/devui/textarea/demo/text-demo.module.ts b/devui/textarea/demo/text-demo.module.ts index fcea0c15..006bef40 100644 --- a/devui/textarea/demo/text-demo.module.ts +++ b/devui/textarea/demo/text-demo.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { FormModule } from 'ng-devui/form'; import { DevUIApiComponent } from 'ng-devui/shared/devui-api/devui-api.component'; import { DevUIApiModule } from 'ng-devui/shared/devui-api/devui-api.module'; import { DevUICodeboxModule } from 'ng-devui/shared/devui-codebox'; @@ -8,6 +9,7 @@ import { TextareaModule } from 'ng-devui/textarea'; import { TranslateModule } from '@ngx-translate/core'; import { DDemoNavModule } from 'src/app/component/d-demo-nav.module'; import { BasicComponent } from './basic/basic.component'; +import { CountComponent } from './count/count.component'; import { ResizeComponent } from './resize/resize.component'; import { TextDemoComponent } from './text-demo.component'; @@ -19,6 +21,7 @@ import { TextDemoComponent } from './text-demo.component'; DevUICodeboxModule, DevUIApiModule, DDemoNavModule, + FormModule, RouterModule.forChild([ { path: '', redirectTo: 'demo' }, { path: 'demo', component: TextDemoComponent}, @@ -32,7 +35,8 @@ import { TextDemoComponent } from './text-demo.component'; declarations: [ TextDemoComponent, BasicComponent, - ResizeComponent + ResizeComponent, + CountComponent ], }) diff --git a/devui/time-axis/demo/alternative-mode/alternative-mode.component.html b/devui/time-axis/demo/alternative-mode/alternative-mode.component.html index 04ed2cfc..7f83e195 100644 --- a/devui/time-axis/demo/alternative-mode/alternative-mode.component.html +++ b/devui/time-axis/demo/alternative-mode/alternative-mode.component.html @@ -1,6 +1,8 @@

horizontal: time item's position can be customized.

- +
+ +

vertical: time item's position can be customized.

diff --git a/devui/time-axis/demo/alternative-mode/alternative-mode.component.ts b/devui/time-axis/demo/alternative-mode/alternative-mode.component.ts index 26a25a7f..b58e281b 100644 --- a/devui/time-axis/demo/alternative-mode/alternative-mode.component.ts +++ b/devui/time-axis/demo/alternative-mode/alternative-mode.component.ts @@ -16,22 +16,22 @@ export class AlternativeModeComponent implements OnInit { { text: 'Check', position: 'bottom', - type: 'success' + dotColor: 'var(--devui-success)' }, { text: 'Build', position: 'top', - type: 'danger' + dotColor: 'var(--devui-danger)' }, { text: 'Depoy', position: 'bottom', - type: 'warning' + dotColor: 'var(--devui-warning)' }, { text: 'End', position: 'top', - type: 'waiting' + dotColor: 'var(--devui-waiting)' }, ] }; @@ -44,19 +44,19 @@ export class AlternativeModeComponent implements OnInit { }, { text: 'Check', - type: 'success' + dotColor: 'var(--devui-success)' }, { text: 'Build', - type: 'danger' + dotColor: 'var(--devui-danger)' }, { text: 'Depoy', - type: 'warning' + dotColor: 'var(--devui-warning)' }, { text: 'End', - type: 'waiting' + dotColor: 'var(--devui-waiting)' }, ] }; diff --git a/devui/time-axis/demo/custom-dot/custom-dot.component.html b/devui/time-axis/demo/custom-dot/custom-dot.component.html index 254ca776..16df22d7 100644 --- a/devui/time-axis/demo/custom-dot/custom-dot.component.html +++ b/devui/time-axis/demo/custom-dot/custom-dot.component.html @@ -1,3 +1,3 @@ -
+
diff --git a/devui/time-axis/demo/direction/time-axis-direction.component.html b/devui/time-axis/demo/direction/time-axis-direction.component.html index 8151b925..7804b012 100644 --- a/devui/time-axis/demo/direction/time-axis-direction.component.html +++ b/devui/time-axis/demo/direction/time-axis-direction.component.html @@ -1,5 +1,7 @@

time-axis direction: horizontal

- +
+ +

time-axis direction: vertical

diff --git a/devui/time-axis/demo/direction/time-axis-direction.component.ts b/devui/time-axis/demo/direction/time-axis-direction.component.ts index c3472145..998d0256 100644 --- a/devui/time-axis/demo/direction/time-axis-direction.component.ts +++ b/devui/time-axis/demo/direction/time-axis-direction.component.ts @@ -15,22 +15,22 @@ export class TimeAxisDirectionComponent implements OnInit { { text: 'Check', time: '2021-07-29', - type: 'success' + dotColor: 'var(--devui-success)' }, { text: 'Build', time: '2021-07-30', - type: 'danger' + dotColor: 'var(--devui-danger)' }, { text: 'Depoy', time: '2021-07-31', - type: 'warning' + dotColor: 'var(--devui-warning)' }, { text: 'End', time: '2021-08-01', - type: 'waiting' + dotColor: 'var(--devui-waiting)' } ] }; @@ -47,25 +47,25 @@ export class TimeAxisDirectionComponent implements OnInit { text: 'Check', time: '2021-07-29', position: 'right', - type: 'success' + dotColor: 'var(--devui-success)' }, { text: 'Build', time: '2021-07-30', position: 'right', - type: 'danger' + dotColor: 'var(--devui-danger)' }, { text: 'Depoy', time: '2021-07-31', position: 'right', - type: 'warning' + dotColor: 'var(--devui-warning)' }, { text: 'End', time: '2021-08-01', position: 'right', - type: 'waiting', + dotColor: 'var(--devui-waiting)', lineStyle: {style: 'none'} } ] diff --git a/devui/time-axis/demo/html-content/time-axis-html-content.component.ts b/devui/time-axis/demo/html-content/time-axis-html-content.component.ts index 220a4dde..b261bee1 100644 --- a/devui/time-axis/demo/html-content/time-axis-html-content.component.ts +++ b/devui/time-axis/demo/html-content/time-axis-html-content.component.ts @@ -19,10 +19,10 @@ export class TimeAxisHtmlContentComponent implements OnInit { direction: 'vertical', list: [ {time: '2017-07-25', text: '
some events in 2017-07-25
', - type: 'success', iconClass: 'stops'}, - {time: '2017-07-27', text: '
some events in 2017-07-27
', type: 'warning'}, - {time: '2017-07-28', text: '
some events in 2017-07-28
', type: 'primary'}, - {time: '2017-07-29', text: '
some events in 2017-07-29
', type: 'info'} + dotColor: 'var(--devui-success)', iconClass: 'stops'}, + {time: '2017-07-27', text: '
some events in 2017-07-27
', dotColor: 'var(--devui-warning)'}, + {time: '2017-07-28', text: '
some events in 2017-07-28
'}, + {time: '2017-07-29', text: '
some events in 2017-07-29
', dotColor: 'var(--devui-info)'} ]}; } } diff --git a/devui/time-axis/demo/seperate-way/seperate-way.component.html b/devui/time-axis/demo/seperate-way/seperate-way.component.html index a9b15f5f..a2c0a110 100644 --- a/devui/time-axis/demo/seperate-way/seperate-way.component.html +++ b/devui/time-axis/demo/seperate-way/seperate-way.component.html @@ -1,9 +1,9 @@

time seperate

-
+
- - - - + + + +
diff --git a/devui/time-axis/demo/template-content/time-axis-template-content.component.html b/devui/time-axis/demo/template-content/time-axis-template-content.component.html index 83d72777..148fe1af 100644 --- a/devui/time-axis/demo/template-content/time-axis-template-content.component.html +++ b/devui/time-axis/demo/template-content/time-axis-template-content.component.html @@ -9,13 +9,13 @@ style="margin-bottom: 4px; position: relative; left: 4px; width: 2px; height: 40px; background-color: #dfe1e6" >
-
{{ data.data.title }}
-
发布日期:{{ data.data.date }}
-
版本状态:
+
{{ data.title }}
+
发布日期:{{ data.date }}
+
版本状态:
; - type?: 'primary' | 'success' | 'danger' | 'warning' | 'waiting' | 'info'; - status?: 'runned' | 'running' | 'error'; + type?: 'primary' | 'success' | 'danger' | 'warning'; + status?: 'runned' | 'running' | ''; position?: 'top' | 'bottom' | 'left' | 'right'; extraElement?: string | HTMLElement| TemplateRef; iconClass?: string; diff --git a/devui/time-axis/doc/api-en.md b/devui/time-axis/doc/api-en.md index bc9fd0fe..0b7194eb 100644 --- a/devui/time-axis/doc/api-en.md +++ b/devui/time-axis/doc/api-en.md @@ -26,6 +26,7 @@ In the page: | direction | `'vertical'\|'horizontal'` | '' | Optional. Sets the time axis direction. | [Setting Direction Parameters](demo#direction) | | position | `'left'\|'bottom'` | '' | Optional. Defines the time parameter position only when direction is `vertical`. | [Setting Time Position](demo#basic-usage) | | widthMode | `'fitContent'\|'fitWidth'` | `'fitContent'` | Optional. Only when the direction is `horizontal`, `widthMode='fitContent'` The timeline width is adaptive to the content width, and `widthMode='fitWidth'` The timeline width fills the container. | [Time Point Customization](demo#custom-dot) | +| horizontalAlign | `'center'\|'left'` | `'center'` | Optional. Only when the direction is `horizontal`, Set Content Alignment. | [Customizing Content Using a Template](demo#content-with-template) | | model | `'text'\|'html'\|'template'` | '' | Optional. model. | [Content Use HTML](demo#content-with-html) | | list | [`array`](#list) | [] | Optional. List Data. | [Setting Direction Parameters](demo#direction) | @@ -33,13 +34,15 @@ In the page: | Parameter | Type | Default | Description | Jump to Demo | | :----: | :-----------------------------------------: | :--: | :----------------------------------------------------------- | ----------------------------------------------- | -| time | `string` | -- | Optional. Time. | [Setting Time Position](demo#basic-usage) | -| text | `string` | -- | Optional. Text Content. | [Setting Time Position](demo#basic-usage) | -| type | `'primary'\|'success'\|'danger'\|'warning'\|'waiting'\|'info'` | `info` | Optional. Time Point Type. | [Setting Direction Parameters](demo#direction) | -| ~~status~~ | `'runned'\|'running'\|'error'` | -- | Optional. Status.(`Deprecated. You are advised to use type.`) | [Setting Time Position](demo#basic-usage) | +| time | `string` | -- | Optional. Time. | [Setting Direction Parameters](demo#direction) | +| text | `string` | -- | Optional. Text Content. | [Setting Direction Parameters](demo#direction) | +| type | `'primary' \| 'success' \| 'danger' \| 'warning'` | `primary` | Optional. Time Point Type. | [Setting Time Position](demo#basic-usage) | +| ~~status~~ | `'runned'\|'running'\|''` | -- | Optional. Status.(`Deprecated. You are advised to use lineStyle.`) | [Setting Time Position](demo#basic-usage) | | data | `array` | -- | Optional. Template data. This parameter is valid only when model is set to template. | [Customizing Content Using a Template](demo#content-with-template) | | position | `'up'\|'bottom'\|'left'\|'right'` | -- | Optional. Indicates the position of text or data. If time exists, the time is in the reverse position. | [Customizing the Content Direction of a Time Node](demo#content-with-alternative-mode) | -| lineStyle | `{style: 'solid' \| 'dashed' \| 'none', color: string}` | `{style: 'solid'}` | Optional. Setting the Timeline Line Style. | [Time Point Customization](demo#custom-dot) | +| dotColor | `string` | -- | Optional. Custom Time Circle Color. | [Customizing Content Using a Template](demo#content-with-template) | +| lineStyle | `{style: 'solid' \| 'dashed' \| 'dotted' \| 'none', color: string}` | `{style: 'solid'}` | Optional. Setting the Timeline Line Style. | [Time Point Customization](demo#custom-dot) | +| dotColor | `string` | -- | Optional. Custom Time Circle Color. | [Customizing Content Using a Template](demo#content-with-template) | | customDot | `string\|HTMLElement\|TemplateRef` | -- | Optional. User-defined time point. | [Time Point Customization](demo#custom-dot) | | extraElement | `string\|HTMLElement\|TemplateRef` | -- | Optional. Customizing Additional Elements Between Two Points in Time. | [Customizing Content Using a Template](demo#content-with-template) | @@ -50,14 +53,16 @@ interface TimeAxisData { direction?: 'vertical' | 'horizontal' | ''; position?: 'bottom' | 'left' | ''; widthMode?: 'fitContent' | 'fitWidth'; + horizontalAlign?: 'center'|'left'; model: 'text' | 'html' | 'template' | ''; list: Array<{ time?: string; text?: string; lineStyle?: object; + dotColor?: string; customDot?: string | HTMLElement| TemplateRef; - type?: 'primary' | 'success' | 'danger' | 'warning' | 'waiting' | 'info'; - status?: 'runned' | 'running' | 'error'; + type?: 'primary' | 'success' | 'danger' | 'warning'; + status?: 'runned' | 'running' | ''; position?: 'top' | 'bottom' | 'left' | 'right'; extraElement?: string | HTMLElement| TemplateRef; iconClass?: string; diff --git a/devui/time-axis/time-axis-item/time-axis-item.component.html b/devui/time-axis/time-axis-item/time-axis-item.component.html index c8a499dc..6a79aa61 100644 --- a/devui/time-axis/time-axis-item/time-axis-item.component.html +++ b/devui/time-axis/time-axis-item/time-axis-item.component.html @@ -9,9 +9,12 @@ [class.devui-time-axis-item-data-vertical-left]="direction === 'vertical'" >
- {{ time }} +
{{ time }}
-
+
@@ -23,7 +26,11 @@ [class.devui-time-axis-item-axis-padding-horizontal]="direction === 'horizontal'" [class.devui-time-axis-item-axis-padding-vertical]="direction === 'vertical'" > -
+
{{ dotText }}
@@ -33,12 +40,7 @@
{{ time }}
-
+
@@ -53,9 +55,12 @@ [class.devui-time-axis-item-data-time-bottom]="timePosition === 'bottom'" >
- {{ time }} +
{{ time }}
-
+
diff --git a/devui/time-axis/time-axis-item/time-axis-item.component.scss b/devui/time-axis/time-axis-item/time-axis-item.component.scss index e9aa169c..561f0407 100644 --- a/devui/time-axis/time-axis-item/time-axis-item.component.scss +++ b/devui/time-axis/time-axis-item/time-axis-item.component.scss @@ -58,6 +58,10 @@ border-bottom-style: solid; } + &-dotted { + border-bottom-style: dotted; + } + &-none { border-bottom-style: none; } @@ -126,6 +130,10 @@ border-left-style: solid; } + &-dotted { + border-left-style: dotted; + } + &-none { border-left-style: none; } @@ -200,36 +208,33 @@ } .devui-time-axis-item-type { - &-info { - border-color: $devui-brand; + &-primary { + border-color: $devui-placeholder; } &-success { border-color: $devui-success; + color: $devui-light-text; + line-height: 16px; + text-align: center; + background-color: $devui-success; } &-danger { border-color: $devui-danger; + color: $devui-light-text; + line-height: 16px; + text-align: center; + background-color: $devui-danger; } &-warning { border-color: $devui-warning; - } - - &-waiting { - border-color: $devui-waiting; - } - - &-primary { - border-color: $devui-placeholder; - } - - &-runned { - border-color: $devui-success; color: $devui-light-text; + font-weight: 700; line-height: 16px; text-align: center; - background-color: $devui-success; + background-color: $devui-warning; } &-running { @@ -238,14 +243,6 @@ text-align: center; animation: devui-time-axis-running 1.5s linear infinite; } - - &-error { - border-color: $devui-danger; - color: $devui-light-text; - line-height: 16px; - text-align: center; - background-color: $devui-danger; - } } @keyframes devui-time-axis-running { @@ -297,3 +294,10 @@ margin-bottom: 0; } } + +.devui-time-axis-item-horizontal-align-center { + position: relative; + width: fit-content; + transform: translateX(-50%); + left: 8px; +} diff --git a/devui/time-axis/time-axis-item/time-axis-item.component.ts b/devui/time-axis/time-axis-item/time-axis-item.component.ts index d1dd2c18..10da0201 100644 --- a/devui/time-axis/time-axis-item/time-axis-item.component.ts +++ b/devui/time-axis/time-axis-item/time-axis-item.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, Input, OnInit, TemplateRef } from '@angular/core'; +import { Component, EventEmitter, HostBinding, Input, OnInit, Output, TemplateRef } from '@angular/core'; @Component({ selector: 'd-time-axis-item', @@ -11,28 +11,65 @@ export class TimeAxisItemComponent implements OnInit { @Input() time; @Input() timePosition; // direction为vertical时time的位置 @Input() position; - @Input() type; @Input() lineStyle; @Input() customDot; + @Input() dotColor; @Input() iconClass; @Input() extraElement; @Input() text; @Input() contentTemplate: TemplateRef; @Input() data; + @Input() horizontalAlign = 'center'; + @Output() statusChanged = new EventEmitter(); + _type; dotText: string; + @Input() + set type(type) { + switch (type) { + case 'success': + this._type = 'success'; + this.dotText = '✓'; + break; + + case 'danger': + this._type = 'danger'; + this.dotText = '✕'; + break; + + case 'warning': + this._type = 'warning'; + this.dotText = '!'; + break; + + case 'primary': + this._type = 'primary'; + this.dotText = ''; + break; + + case 'running': + this._type = 'running'; + this.dotText = '↻'; + break; + + default: + break; + } + } + /** * @deprecated Use type to replace. */ @Input() set status(status) { + if (status !== undefined) { + this.statusChanged.emit(status); - if (status === 'runned') { - this.type = 'runned'; - } else if (status === 'running') { - this.type = 'running'; - } else if (status === 'error') { - this.type = 'error'; + if (status === 'running') { + this.type = 'running'; + } else if (this._type === 'running' && status === '') { + this.type = 'primary'; + } } } @@ -50,14 +87,6 @@ export class TimeAxisItemComponent implements OnInit { if (this.position === undefined) { this.position = (this.direction === 'vertical' ? 'right' : 'bottom'); } - - if (this.type === 'runned') { - this.dotText = '✓'; - } else if (this.type === 'running') { - this.dotText = '↻'; - } else if (this.type === 'error') { - this.dotText = '✕'; - } } get extraTemplate() { @@ -68,4 +97,10 @@ export class TimeAxisItemComponent implements OnInit { return this.customDot instanceof TemplateRef ? this.extraElement : null; } + get timeAxisLineClass() { + let styleClass = `devui-time-axis-line-style-${ this.lineStyle?.style || 'solid' }`; + styleClass += this.timePosition !== 'bottom' ? ' devui-time-axis-item-line' : ' devui-time-axis-item-line-time-bottom'; + return styleClass; + } + } diff --git a/devui/time-axis/time-axis.component.html b/devui/time-axis/time-axis.component.html index 3b964417..dff0a1a3 100644 --- a/devui/time-axis/time-axis.component.html +++ b/devui/time-axis/time-axis.component.html @@ -11,15 +11,18 @@ [time]="list.time" [timePosition]="data.position" [position]="list.position" - [lineStyle]="list.lineStyle ? list.lineStyle : { style: 'solid' }" + [lineStyle]="list.lineStyle" [customDot]="list.customDot" + [dotColor]="list.dotColor" [iconClass]="list.iconClass" [type]="list.type" [status]="list.status" [extraElement]="list.extraElement" [text]="list.text" [contentTemplate]="contentTemplate" - [data]="list" + [data]="list.data" + [horizontalAlign]="data.horizontalAlign || 'center'" + (statusChanged)="changeStatusLine($event)" > diff --git a/devui/time-axis/time-axis.component.ts b/devui/time-axis/time-axis.component.ts index 47ac5a7e..305df1fb 100644 --- a/devui/time-axis/time-axis.component.ts +++ b/devui/time-axis/time-axis.component.ts @@ -24,6 +24,8 @@ export class TimeAxisComponent implements AfterContentInit, AfterViewInit { if (this.mode === 'alternative') { this.updateAlternativePosition(); } + + this.vanishHorizontalLastLine(); } ngAfterViewInit() { @@ -71,4 +73,33 @@ export class TimeAxisComponent implements AfterContentInit, AfterViewInit { } } + vanishHorizontalLastLine() { + if (this._direction === 'horizontal') { + if (this.data === undefined) { + if (this.listOfItems.last.lineStyle === undefined) { + this.listOfItems.last.lineStyle = { style: 'none' }; + } + } else { + if (this.data.list[this.data.list.length - 1].lineStyle === undefined) { + this.data.list[this.data.list.length - 1].lineStyle = { style: 'none' }; + } + } + } + } + + changeStatusLine(event) { + if (this.data !== undefined && this.data.direction === 'horizontal') { + setTimeout(() => { + this.data.list.forEach((item, index, array) => { + if (item.status === 'runned' && index > 0) { + array[index - 1].lineStyle = { style: 'solid', color: 'var(--devui-success)' }; + } else if (item.status === 'running' && index > 0) { + array[index - 1].lineStyle = { style: 'dotted', color: 'var(--devui-success)' }; + } else if (item.status === '' && index > 0) { + array[index - 1].lineStyle = { style: 'solid' }; + } + }); + }); + } + } } diff --git a/devui/time-axis/time-axis.spec.ts b/devui/time-axis/time-axis.spec.ts index 4fad4ec8..15c4c25c 100644 --- a/devui/time-axis/time-axis.spec.ts +++ b/devui/time-axis/time-axis.spec.ts @@ -17,7 +17,7 @@ class TestTimeAxisComponent { { time: '2017-07-27', text: '这是2017-07-27发生的事件', type: 'success' }, { time: '2017-07-28', text: '这是2017-07-28发生的事件', type: 'danger' }, { time: '2017-07-29', text: '这是2017-07-29发生的事件', type: 'warning' }, - { time: '2017-07-29', text: '这是2017-07-29发生的事件', type: 'waiting' }, + { time: '2017-07-29', text: '这是2017-07-29发生的事件', type: 'warning' }, ], }; } @@ -81,7 +81,7 @@ describe('time-axis base', () => { fixture.detectChanges(); // 最后两个tr元素中的状态样式 expect(lis[3].querySelector('.devui-time-axis-item-dot')).not.toBeNull(); - expect(lis[4].querySelector('.devui-time-axis-item-type-waiting')).not.toBeNull(); + expect(lis[4].querySelector('.devui-time-axis-item-type-warning')).not.toBeNull(); }); }); }); diff --git a/devui/time-axis/time-axis.type.ts b/devui/time-axis/time-axis.type.ts index 13f33278..91c16d7f 100644 --- a/devui/time-axis/time-axis.type.ts +++ b/devui/time-axis/time-axis.type.ts @@ -4,14 +4,16 @@ export interface TimeAxisData { direction?: 'vertical' | 'horizontal' | ''; position?: 'bottom' | 'left' | ''; widthMode?: 'fitContent' | 'fitWidth'; + horizontalAlign?: 'center'|'left'; model: 'text' | 'html' | 'template' | ''; list: Array<{ time?: string; text?: string; lineStyle?: object; + dotColor?: string; customDot?: string | HTMLElement| TemplateRef; - type?: 'primary' | 'success' | 'danger' | 'warning' | 'waiting' | 'info'; - status?: 'runned' | 'running' | 'error'; + type?: 'primary' | 'success' | 'danger' | 'warning'; + status?: 'runned' | 'running' | ''; position?: 'top' | 'bottom' | 'left' | 'right'; extraElement?: string | HTMLElement| TemplateRef; iconClass?: string; diff --git a/devui/time-picker/time-picker.component.ts b/devui/time-picker/time-picker.component.ts index 7b99327d..58a6456d 100644 --- a/devui/time-picker/time-picker.component.ts +++ b/devui/time-picker/time-picker.component.ts @@ -1,4 +1,5 @@ import { CdkOverlayOrigin, ConnectedOverlayPositionChange, ConnectedPosition, VerticalConnectionPos } from '@angular/cdk/overlay'; +import { DOCUMENT } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -6,6 +7,7 @@ import { EventEmitter, forwardRef, HostListener, + Inject, Input, OnChanges, OnDestroy, @@ -97,6 +99,7 @@ export class TimePickerComponent implements OnChanges, OnInit, OnDestroy, Contro private _illegalTimeMin = '60'; private _illegalTimeSec = '60'; private correct = ['Hour', 'Min', 'Sec']; + document: Document; private onChange = (_: any) => null; private onTouched = () => null; @@ -150,7 +153,7 @@ export class TimePickerComponent implements OnChanges, OnInit, OnDestroy, Contro if (!open) { this.startAnimation = false; removeClassFromOrigin(this.elementRef); - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } else { if (this.timePickerWidth !== undefined) { this.originWidth = this.timePickerWidth; @@ -167,7 +170,7 @@ export class TimePickerComponent implements OnChanges, OnInit, OnDestroy, Contro } setTimeout(() => { this.startAnimation = true; - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); }); addClassToOrigin(this.elementRef); } @@ -207,7 +210,10 @@ export class TimePickerComponent implements OnChanges, OnInit, OnDestroy, Contro private i18n: I18nService, private cdr: ChangeDetectorRef, private devConfigService: DevConfigService, - ) {} + @Inject(DOCUMENT) private doc: any + ) { + this.document = this.doc; + } @HostListener('blur', ['$event']) onBlur($event) { @@ -251,7 +257,7 @@ export class TimePickerComponent implements OnChanges, OnInit, OnDestroy, Contro if (this.userInputSubscription) { this.userInputSubscription.unsubscribe(); } - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); } registerOnChange(fn: any): void { @@ -551,6 +557,9 @@ export class TimePickerComponent implements OnChanges, OnInit, OnDestroy, Contro } scrollTo(element: HTMLElement, to: number, duration: number): void { + if (typeof window === 'undefined') { + return; + } if (duration <= 0) { element.scrollTop = to; return; diff --git a/devui/toast/demo/service/toast-service.component.html b/devui/toast/demo/service/toast-service.component.html index d55cac79..51407911 100644 --- a/devui/toast/demo/service/toast-service.component.html +++ b/devui/toast/demo/service/toast-service.component.html @@ -1,4 +1,4 @@ click me show simplest toast! -click me show customer toast! -click me close customer toast! +click me show customer toast! +click me close customer toast! only close first customer toast! diff --git a/devui/toggle/toggle.component.scss b/devui/toggle/toggle.component.scss index b0e40a0d..c074ece1 100755 --- a/devui/toggle/toggle.component.scss +++ b/devui/toggle/toggle.component.scss @@ -46,6 +46,7 @@ width: 100%; height: 100%; text-align: center; + color: $devui-light-text; } } diff --git a/devui/tooltip/demo/basic/basic.component.html b/devui/tooltip/demo/basic/basic.component.html index 91818a22..50cf7d17 100755 --- a/devui/tooltip/demo/basic/basic.component.html +++ b/devui/tooltip/demo/basic/basic.component.html @@ -1,9 +1,9 @@
- left - top - bottom - right - No Animation + left + top + bottom + right + No Animation
diff --git a/devui/tooltip/demo/delay/delay.component.html b/devui/tooltip/demo/delay/delay.component.html index a51f5091..2abab6eb 100644 --- a/devui/tooltip/demo/delay/delay.component.html +++ b/devui/tooltip/demo/delay/delay.component.html @@ -1,6 +1,8 @@
MouseEnter delay 500ms - MouseLeave delay 1000ms + + MouseLeave delay 1000ms +
diff --git a/devui/tooltip/tooltip.directive.ts b/devui/tooltip/tooltip.directive.ts index bbdcc78a..36da5eb5 100755 --- a/devui/tooltip/tooltip.directive.ts +++ b/devui/tooltip/tooltip.directive.ts @@ -8,7 +8,7 @@ import { Input, OnChanges, OnDestroy, - SimpleChanges, + SimpleChanges } from '@angular/core'; import { OverlayContainerRef } from 'ng-devui/overlay-container'; import { DevConfigService, WithConfig } from 'ng-devui/utils/globalConfig'; @@ -145,7 +145,6 @@ export class TooltipDirective implements OnChanges, AfterViewInit, OnDestroy { keyArr.forEach((item) => (obj[item] = this[item])); Object.assign(this.tooltipComponentRef.instance, obj); } - ngOnChanges(changes: SimpleChanges): void { if (this.tooltipComponentRef) { const { content, position, showAnimation } = changes; diff --git a/devui/transfer/demo/search/transfer-demo-search.component.html b/devui/transfer/demo/search/transfer-demo-search.component.html index 8e3af10e..d42022a9 100644 --- a/devui/transfer/demo/search/transfer-demo-search.component.html +++ b/devui/transfer/demo/search/transfer-demo-search.component.html @@ -3,6 +3,7 @@ [disabled]="disabled" [sourceOption]="sourceOption1" [isSearch]="true" + [showOptionTitle]="true" [titles]="{ source: 'Source', target: 'Target' }" (afterTransfer)="afterTransfer($event)" > diff --git a/devui/transfer/doc/api-cn.md b/devui/transfer/doc/api-cn.md index 1d278d8b..2bc64928 100644 --- a/devui/transfer/doc/api-cn.md +++ b/devui/transfer/doc/api-cn.md @@ -18,11 +18,14 @@ import { TransferModule } from 'ng-devui/transfer'; | targetOption | `array` | [] | 可选参数,穿梭框目标数据 | [基本用法](demo#transfer-demo-base) | | titles | `array` | [] | 可选参数,穿梭框标题 | [基本用法](demo#transfer-demo-base) | | height | `string` | 320px | 可选参数,穿梭框高度 | -| isSearch | `number` | false | 可选参数,是否可以搜索 | [搜索穿梭框](demo#transfer-demo-search) | +| isSearch | `boolean` | false | 可选参数,是否可以搜索 | [搜索穿梭框](demo#transfer-demo-search) | | isSourceDroppable | `boolean` | false | 可选参数,源是否可以拖拽 | | isTargetDroppable | `boolean` | false | 可选参数,目标是否可以拖拽 | [排序穿梭框](demo#transfer-demo-sort) | | disabled | `boolean` | false | 可选参数 穿梭框禁止使用 | [基本用法](demo#transfer-demo-base) | +| showOptionTitle | `boolean` | false | 可选,鼠标悬浮于数据是否显示title | [搜索穿梭框](demo#transfer-demo-search) | | beforeTransfer | `(sourceOption, targetOption) => boolean \| Promise \| Observable` | - | 可选参数 在transfer事件发生前判断事件是否允许触发 | [基本用法](demo#transfer-demo-base) | +|customSourceCheckedLen|`number`| 0 |可选,使用模板时判断源是否可穿梭| [自定义穿梭框](demo#transfer-demo-custom) | +|customTargetCheckedLen|`number`| 0 |可选,使用模板时判断目标是否可穿梭| [自定义穿梭框](demo#transfer-demo-custom) | ## d-transfer 事件 diff --git a/devui/transfer/doc/api-en.md b/devui/transfer/doc/api-en.md index b3a6073b..c3b59d55 100644 --- a/devui/transfer/doc/api-en.md +++ b/devui/transfer/doc/api-en.md @@ -18,11 +18,14 @@ In the page: | targetOption | `array` | [] | Optional. This parameter indicates the target data of the shuttle box. | [Basic Usage](demo#transfer-demo-base) | | titles | `array` | [] | Optional. Title of the shuttle box. | [Basic Usage](demo#transfer-demo-base) | | height | `string` | 320px | Optional. It indicates the height of the shuttle box. | -| isSearch | `number` | false | Optional. Specifies whether to search. | [Search Shuttle Box](demo#transfer-demo-search) | +| isSearch | `boolean` | false | Optional. Specifies whether to search. | [Search Shuttle Box](demo#transfer-demo-search) | | isSourceDroppable | `boolean` | false | Optional. Indicates whether the source can be dragged. | | isTargetDroppable | `boolean` | false | Optional. Indicates whether the object can be dragged. | [Sorting Shuttle Box](demo#transfer-demo-sort) | | disabled | `boolean` | false | Optional. The shuttle box cannot be used. | [Basic Usage](demo#transfer-demo-base) | +| showOptionTitle | `boolean` | false | Optional. Indicates whether to display title when the cursor is hovered over data. | [Search Shuttle Box](demo#transfer-demo-search) | | beforeTransfer | `(sourceOption, targetOption) => boolean \| Promise \| Observable` | - | Optional. Determines whether the transfer event can be triggered before the transfer event occurs. | [Basic Usage](demo#transfer-demo-base) | +|customSourceCheckedLen|`number`| 0 |Optional. Determine whether the source can be shuttled when using a template.| [Custom Shuttle Box](demo#transfer-demo-custom) | +|customTargetCheckedLen|`number`| 0 |Optional. Determine whether the target can be shuttled when using a template.| [Custom Shuttle Box](demo#transfer-demo-custom) | ## d-transfer event diff --git a/devui/transfer/transfer.component.html b/devui/transfer/transfer.component.html index 217d03c0..8f15e2c9 100644 --- a/devui/transfer/transfer.component.html +++ b/devui/transfer/transfer.component.html @@ -62,6 +62,11 @@ boolean | Promise | Observable; // 自定义 @@ -106,9 +107,11 @@ export class TransferComponent implements OnInit, OnChanges, OnDestroy { } ngOnChanges(changes: SimpleChanges): void { - if (changes && (changes.customSourceCheckedLen || changes.customTargetCheckedLen)) { - this.targetCanTransfer = !!(changes.customSourceCheckedLen && changes.customSourceCheckedLen.currentValue > 0); - this.sourceCanTransfer = !!(changes.customTargetCheckedLen && changes.customTargetCheckedLen.currentValue > 0); + if (changes && changes.customSourceCheckedLen) { + this.targetCanTransfer = !!(this.customSourceCheckedLen > 0); + } + if (changes && changes.customTargetCheckedLen) { + this.sourceCanTransfer = !!(this.customTargetCheckedLen > 0); } if (changes && (changes.sourceOption || changes.targetOption)) { @@ -174,9 +177,12 @@ export class TransferComponent implements OnInit, OnChanges, OnDestroy { if (this.transferring.observers.length) { this.transferring.emit(direction); + setTimeout(() => { + this.transferHandle(direction); + }); } else { + const changeData = []; if (direction === TransferDirection.TARGET) { - const changeData = []; // 对源数据更改 this.sourceDisplayOption.filter(item => item.checked === true).forEach(item => { const tmp = { name: item.name, value: item.value, id: item.id, checked: false }; @@ -184,18 +190,7 @@ export class TransferComponent implements OnInit, OnChanges, OnDestroy { changeData.push(tmp); this.sourceOption.splice(this.sourceOption.indexOf(item), 1); }); - this.targetCanTransfer = false; - - if (this.sourceCustomViewTemplate) { - this.transferToTarget.next(); - } else { - this.transferToTarget.next({ sourceOption: this.sourceOption, targetOption: this.targetOption, changeData }); - } - if (this.isSearch && this.sourceSearchText !== '') { - this.sourceSearchText = ''; - } } else if (direction === TransferDirection.SOURCE) { - const changeData = []; this.targetDisplayOption.filter(item => item.checked === true).forEach(item => { const tmp = { name: item.name, value: item.value, id: item.id, checked: false }; this.sourceOption.push(tmp); @@ -203,26 +198,49 @@ export class TransferComponent implements OnInit, OnChanges, OnDestroy { this.targetOption.splice(this.targetOption.indexOf(item), 1); }); this.targetOption = this.targetOption.filter(item => item.checked !== true); - this.sourceCanTransfer = false; - if (this.targetCustomViewTemplate) { - this.transferToSource.next(); - } else { - this.transferToSource.next({ sourceOption: this.sourceOption, targetOption: this.targetOption, changeData }); - } - - if (this.isSearch && this.targetSearchText !== '') { - this.targetSearchText = ''; - } } - this.targetDisplayOption = this.targetOption; - this.sourceDisplayOption = this.sourceOption; - this.listTotalCheck(TransferDirection.TARGET); - this.listTotalCheck(TransferDirection.SOURCE); - this.afterTransfer.emit(direction); + + this.transferHandle(direction, changeData); } }); } + transferHandle(direction: TransferDirection, changeData?: object) { + if (direction === TransferDirection.TARGET) { + this.targetCanTransfer = false; + + if (this.sourceCustomViewTemplate) { + this.transferToTarget.next(); + } else { + changeData === undefined + ? this.transferToTarget.next({ sourceOption: this.sourceOption, targetOption: this.targetOption }) + : this.transferToTarget.next({ sourceOption: this.sourceOption, targetOption: this.targetOption, changeData }); + } + if (this.isSearch && this.sourceSearchText !== '') { + this.sourceSearchText = ''; + } + } else if (direction === TransferDirection.SOURCE) { + this.sourceCanTransfer = false; + + if (this.targetCustomViewTemplate) { + this.transferToSource.next(); + } else { + changeData === undefined + ? this.transferToSource.next({ sourceOption: this.sourceOption, targetOption: this.targetOption }) + : this.transferToSource.next({ sourceOption: this.sourceOption, targetOption: this.targetOption, changeData }); + } + if (this.isSearch && this.targetSearchText !== '') { + this.targetSearchText = ''; + } + } + + this.targetDisplayOption = this.targetOption; + this.sourceDisplayOption = this.sourceOption; + this.listTotalCheck(TransferDirection.TARGET); + this.listTotalCheck(TransferDirection.SOURCE); + this.afterTransfer.emit(direction); + } + checkAll(direction: TransferDirection, event: any) { if (direction === TransferDirection.SOURCE) { if (event) { diff --git a/devui/transfer/transfer.module.ts b/devui/transfer/transfer.module.ts index 6ed1518f..e1e25f2a 100644 --- a/devui/transfer/transfer.module.ts +++ b/devui/transfer/transfer.module.ts @@ -1,15 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; - -import { TransferComponent } from './transfer.component'; - import { CheckBoxModule } from 'ng-devui/checkbox'; import { DragDropModule } from 'ng-devui/dragdrop'; +import { PopoverModule } from 'ng-devui/popover'; import { SearchModule } from 'ng-devui/search'; +import { TransferComponent } from './transfer.component'; @NgModule({ - imports: [CommonModule, FormsModule, SearchModule, CheckBoxModule, DragDropModule], + imports: [CommonModule, FormsModule, SearchModule, CheckBoxModule, DragDropModule, PopoverModule], exports: [TransferComponent], declarations: [TransferComponent], providers: [] diff --git a/devui/tree-select/tree-select.component.scss b/devui/tree-select/tree-select.component.scss index 39cf0369..ad36aa94 100755 --- a/devui/tree-select/tree-select.component.scss +++ b/devui/tree-select/tree-select.component.scss @@ -30,7 +30,6 @@ } &.devui-tree-select-input { - color: $devui-line; //TODO: Color-Question font-size: $devui-font-size; padding: 2px 32px 2px 2px; height: unset; @@ -39,6 +38,7 @@ overflow-y: scroll; line-height: 22px; display: flex; + text-indent: 8px; &.devui-select-no-label { padding-left: 10px; @@ -167,6 +167,7 @@ margin: 1px; padding: 0 5px; position: relative; + text-indent: 0; &.devui-no-label-item { padding: 1px 0; @@ -240,6 +241,8 @@ svg.svg-icon-search path { } .devui-select-input.devui-tree-select-input.disabled { + color: $devui-disabled-text; + & .devui-select-item { background-color: $devui-disabled-bg; border-color: $devui-disabled-line; @@ -274,3 +277,11 @@ svg.svg-icon-search path { } } } + +.devui-tree-select { + .devui-select-input.devui-tree-select-input { + &:not(.icon):not([class^=icon-]):not([disabled]):not(.disabled):not(.devui-disabled) { // 为了增加层级覆盖form的样式 + color: $devui-placeholder; + } + } +} diff --git a/devui/tree-select/tree-select.component.ts b/devui/tree-select/tree-select.component.ts index ed4e1384..d1020418 100755 --- a/devui/tree-select/tree-select.component.ts +++ b/devui/tree-select/tree-select.component.ts @@ -232,6 +232,9 @@ export class TreeSelectComponent implements ControlValueAccessor, OnInit, AfterV } private queryMedia() { + if (typeof window === 'undefined') { + return; + } const userAgent = window.navigator.userAgent; if (userAgent.indexOf('Edge') > -1) { this.userAgent = 'edge'; @@ -426,7 +429,8 @@ export class TreeSelectComponent implements ControlValueAccessor, OnInit, AfterV if (this.multiple) { this.tree.treeFactory.checkNodesById(item.id, false); const curValue = this.tree.treeFactory.getCheckedNodes(); - this.value = curValue.map(node => node.data.originItem); + this.value = this.leafOnly + ? curValue.filter(node => !node.data.isParent).map(node => node.data.originItem) : curValue.map(node => node.data.originItem); } else { this.clearAll(); } diff --git a/devui/tree/demo/checkable/checkable.component.html b/devui/tree/demo/checkable/checkable.component.html index bec2066c..a8a52753 100755 --- a/devui/tree/demo/checkable/checkable.component.html +++ b/devui/tree/demo/checkable/checkable.component.html @@ -7,6 +7,8 @@

Operable Tree

[checkboxDisabledKey]="'disabled'" [toggleDisabledKey]="'disableToggle'" [selectDisabledKey]="'disableSelect'" + [canActivateNode]="false" + [canActivateParentNode]="false" (nodeDeleted)="onOperableNodeDeleted($event)" (nodeSelected)="onOperableNodeSelected($event)" (nodeDblClicked)="onOperableNodeDblClicked($event)" diff --git a/devui/tree/demo/draggable/draggable.component.html b/devui/tree/demo/draggable/draggable.component.html index b8b1a964..5697b537 100755 --- a/devui/tree/demo/draggable/draggable.component.html +++ b/devui/tree/demo/draggable/draggable.component.html @@ -1,4 +1,4 @@ -

Default

+

Default

Default (nodeToggled)="showNode($event)" > -

Sortable

+

Sortable

{ console.log('dragNodeId: ' + dragNodeId); console.log('dropNodeId: ' + dropNodeId); - resovle(); + resovle(undefined); }); } diff --git a/devui/tree/demo/operate-btn/operate-btn.component.html b/devui/tree/demo/operate-btn/operate-btn.component.html index e42d780f..f9e1fb2e 100755 --- a/devui/tree/demo/operate-btn/operate-btn.component.html +++ b/devui/tree/demo/operate-btn/operate-btn.component.html @@ -12,8 +12,6 @@ [addable]="true" [editable]="true" [deletable]="true" - [canActivateNode]="false" - [canActivateParentNode]="false" [beforeAddNode]="beforeAddNode" [beforeDeleteNode]="beforeDeleteNode" [beforeEditNode]="beforeEditNode" diff --git a/devui/tree/doc/api-cn.md b/devui/tree/doc/api-cn.md index 11d16d90..cba16108 100644 --- a/devui/tree/doc/api-cn.md +++ b/devui/tree/doc/api-cn.md @@ -72,8 +72,8 @@ import { TreeModule } from 'ng-devui/tree'; | deletable | `boolean` | false | 可选,是否显示删除子节点按钮 | [操作按钮](demo#operation-button) | | draggable | `boolean` | false | 可选,树节点是否支持 drag、drop 操作 | [可拖拽树](demo#drag-and-drop-tree) | | checkboxInput | [`ICheckboxInput`](#icheckboxinput) | {} | 可选,设置 checkbox 的相关属性 | [可勾选树](demo#checkable-tree) | -| canActivateNode | `boolean` | true | 可选,是否可以选中节点 ,false 时点击节点触发 nodeChecked 事件,不触发 nodeSelected 事件 | [操作按钮](demo#operation-button) | -| canActivateParentNode | `boolean` | true | 可选,父节点是否可选中,false 时点击节点触发 nodeChecked 事件,不触发 nodeSelected 事件 | [操作按钮](demo#operation-button) | +| canActivateNode | `boolean` | true | 可选,是否可以选中节点 ,false 时点击节点触发 nodeChecked 事件,不触发 nodeSelected 事件 | [可勾选树](demo#checkable-tree) | +| canActivateParentNode | `boolean` | true | 可选,父节点是否可选中,false 时点击节点触发 nodeChecked 事件,不触发 nodeSelected 事件 | [可勾选树](demo#checkable-tree) | | iconTemplatePosition | `string` | -- | 可选,设置图标的位置,可选`'before-checkbox'`或`'after-checkbox'` | [自定义图标](demo#custom-icon) | | checkableRelation | `'upward' \| 'downward' \| 'both' \| 'none'` | 'both' | 可选,设置父子节点的 check 规则 | [控制父子 check 关系](demo#check-control-tree) | | beforeAddNode | `Promise` | -- | 可选,新增子节点前回调(参数为当前节点), 返回值中可指定添加节点的 index | [操作按钮](demo#operation-button) | diff --git a/devui/tree/doc/api-en.md b/devui/tree/doc/api-en.md index 9d7f9624..e70c9e86 100644 --- a/devui/tree/doc/api-en.md +++ b/devui/tree/doc/api-en.md @@ -72,8 +72,8 @@ In the page: | deleteable | `boolean` | false | Optional. Whether to display the button for deleting subnodes. | [Operation button](demo#operation-button) | | draggable | `boolean` | false | Optional. indicating whether a tree node supports drag and drop operations. | [Draggable tree](demo#drag-and-drop-tree) | | checkboxInput | `ICheckboxInput` | {} | Optional. Sets the attributes of the checkbox. | [Checkable Tree](demo#checkable-tree) | -| canActivateNode | `boolean` | true | : indicates whether a node can be selected. If the value is false, the nodeChecked event is triggered when a node is clicked. | [Operation button](demo#operation-button) | -| canActivateParentNode | `boolean` | true | Optional. indicates whether a parent node is optional. If the value is false, the nodeChecked event is triggered when a node is clicked. | [Operation button](demo#operation-button) | +| canActivateNode | `boolean` | true | : indicates whether a node can be selected. If the value is false, the nodeChecked event is triggered when a node is clicked. | [Checkable Tree](demo#checkable-tree) | +| canActivateParentNode | `boolean` | true | Optional. indicates whether a parent node is optional. If the value is false, the nodeChecked event is triggered when a node is clicked. | [Checkable Tree](demo#checkable-tree) | | iconTemplatePosition | `string` | -- | Optional. Sets the position of the icon, which can be `before-checkbox'` or `after-checkbox'` | [Custom Icon](demo#custom-icon) | | checkableRelation | `'upward' \|'downward' \|'both' \|'none'` | 'both' | Optional. Sets the check rule of the parent-child node | [Control the parent-child check relationship](demo#check-control-tree) | | beforeAddNode | `Promise` | -- | Optional. Call back before adding a subnode (the parameter is the current node). The return value can specify the index of the added node. | [Operation button](demo#operation-button) | diff --git a/devui/tree/operable-tree.component.ts b/devui/tree/operable-tree.component.ts index 9f2a1107..13a47e3e 100755 --- a/devui/tree/operable-tree.component.ts +++ b/devui/tree/operable-tree.component.ts @@ -347,6 +347,9 @@ export class OperableTreeComponent implements OnInit, OnDestroy, AfterViewInit { } addResult.then((nodeInfo) => { + if (!nodeInfo) { + return; + } const node = this.treeFactory.addNode({ parentId: treeNode.id, title: nodeInfo['title'] ? nodeInfo['title'] : '新增节点', diff --git a/devui/tree/tree-factory.class.ts b/devui/tree/tree-factory.class.ts index f46a045c..742f445a 100755 --- a/devui/tree/tree-factory.class.ts +++ b/devui/tree/tree-factory.class.ts @@ -192,7 +192,7 @@ export class TreeFactory { this.nodes[id].data.editable = true; } - deleteNodeById(id: number | string) { + deleteNodeById(id: number | string, renderTree = true) { const node = this.nodes[id]; const parentNode = this.nodes[node.parentId]; this.removeChildNode(parentNode, node); @@ -211,7 +211,9 @@ export class TreeFactory { if (parentNode && (!parentNode.data.children || !parentNode.data.children.length)) { parentNode.data.isParent = false; } - this.renderFlattenTree(); + if (renderTree) { + this.renderFlattenTree(); + } return this; } diff --git a/devui/upload/demo/basic/basic.component.ts b/devui/upload/demo/basic/basic.component.ts index 06c930e6..5e663177 100755 --- a/devui/upload/demo/basic/basic.component.ts +++ b/devui/upload/demo/basic/basic.component.ts @@ -1,6 +1,7 @@ // 注意需要在使用的NgModule中 import { HttpClientModule } from '@angular/common/http'; +import { DOCUMENT } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { IFileOptions, IUploadOptions, SingleUploadComponent } from 'ng-devui/upload'; @Component({ @@ -36,7 +37,7 @@ export class BasicComponent implements OnInit { selectedFiles: any; - constructor(private http: HttpClient) { + constructor(private http: HttpClient, @Inject(DOCUMENT) private doc: any) { this.beforeUploadFn = this.beforeUpload2.bind(this); } @@ -57,7 +58,7 @@ export class BasicComponent implements OnInit { return uploadOptions; } ngOnInit() { - document.getElementById('fileInput').addEventListener('change', event => { + this.doc.getElementById('fileInput').addEventListener('change', event => { this.selectedFiles = (event.target as HTMLInputElement).files; }); } diff --git a/devui/upload/select-files.utils.ts b/devui/upload/select-files.utils.ts index d118387f..16cbd03d 100755 --- a/devui/upload/select-files.utils.ts +++ b/devui/upload/select-files.utils.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; import { I18nInterface, I18nService } from 'ng-devui/i18n'; import { from, Observable, Subscription } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import { mergeMap } from 'rxjs/operators'; import { IFileOptions, IUploadOptions } from './file-uploader.types'; @Injectable() @@ -10,7 +11,10 @@ export class SelectFiles { BEYOND_MAXIMAL_FILE_SIZE_MSG: string; i18nText: I18nInterface['upload']; i18nSubscription: Subscription; - constructor(private i18n: I18nService) { + document: Document; + + constructor(private i18n: I18nService, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; this.i18nText = this.i18n.getI18nText().upload; this.i18nSubscription = this.i18n.langChange().subscribe((data) => { this.i18nText = data.upload; @@ -19,11 +23,11 @@ export class SelectFiles { selectFiles = ({ multiple, accept }: IFileOptions): Promise => { return new Promise((resolve) => { - const tempNode = document.getElementById('d-upload-temp'); + const tempNode = this.document.getElementById('d-upload-temp'); if (tempNode) { - document.body.removeChild(tempNode); + this.document.body.removeChild(tempNode); } - const input = document.createElement('input'); + const input = this.document.createElement('input'); input.style.position = 'fixed'; input.style.left = '-2000px'; @@ -41,7 +45,7 @@ export class SelectFiles { input.addEventListener('change', event => { resolve(Array.prototype.slice.call((event.target as HTMLInputElement).files)); }); - document.body.appendChild(input); // Fix compatibility issue with Internet Explorer 11 + this.document.body.appendChild(input); // Fix compatibility issue with Internet Explorer 11 this.simulateClickEvent(input); }); } @@ -112,7 +116,7 @@ export class SelectFiles { } simulateClickEvent(input) { - const evt = document.createEvent('MouseEvents'); + const evt = this.document.createEvent('MouseEvents'); evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); input.dispatchEvent(evt); } diff --git a/devui/upload/upload.class.ts b/devui/upload/upload.class.ts index 8a338b60..f67f89d3 100755 --- a/devui/upload/upload.class.ts +++ b/devui/upload/upload.class.ts @@ -68,15 +68,13 @@ export class UploadComponent { oneTimeUpload() { const uploads = this.fileUploaders .filter((fileUploader) => fileUploader.status !== UploadStatus.uploaded); - let finalUploads = []; - this.dealOneTimeUploadFiles(uploads).then(result => finalUploads = result); - if (uploads.length > 0) { - return from(finalUploads); - } - return from(Promise.reject('no files')); + return from(this.dealOneTimeUploadFiles(uploads)); } async dealOneTimeUploadFiles(uploads) { + if (!uploads || !uploads.length) { + return Promise.reject('no files'); + } // 触发文件上传 let finalUploads = []; await uploads[0].send(uploads).finally(() => diff --git a/devui/utils/animations/pop-in-out.ts b/devui/utils/animations/pop-in-out.ts index 36e63d33..1d917fdd 100644 --- a/devui/utils/animations/pop-in-out.ts +++ b/devui/utils/animations/pop-in-out.ts @@ -3,8 +3,8 @@ */ import { animate, AnimationTriggerMetadata, state, style, transition, trigger } from '@angular/animations'; -const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || - window.navigator.userAgent.indexOf('Trident') > -1; +const isIE = typeof window !== 'undefined' && (window.navigator.userAgent.indexOf('MSIE ') > -1 || + window.navigator.userAgent.indexOf('Trident') > -1); const ANIMATION = [ state('void', style({transform: 'scale(0)', transformOrigin: '50% 50%'})), diff --git a/devui/utils/animations/scrollAnimation.ts b/devui/utils/animations/scrollAnimation.ts index bc298a8f..fe48779b 100644 --- a/devui/utils/animations/scrollAnimation.ts +++ b/devui/utils/animations/scrollAnimation.ts @@ -1,4 +1,7 @@ export function scrollAnimate(target, currentTopValue, targetTopValue, timeGap: number = 40, scrollTime: number = 450, callback?) { + if (typeof document === 'undefined' || typeof window === 'undefined') { + return; + } const startTimeStamp = Date.now(); const drawAnimateFrame = () => { const currentTime = Date.now() - startTimeStamp; diff --git a/devui/utils/highlight/highlight.component.ts b/devui/utils/highlight/highlight.component.ts index 12ce8345..633877aa 100644 --- a/devui/utils/highlight/highlight.component.ts +++ b/devui/utils/highlight/highlight.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, Inject, Input, OnChanges, SimpleChanges } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ @@ -15,7 +16,11 @@ export class HighlightComponent implements OnChanges { * @deprecated */ @Input() highlightClass = 'devui-match-highlight'; - constructor(private translateHtml: DomSanitizer, private eleRef: ElementRef) {} + document: Document; + + constructor(private translateHtml: DomSanitizer, private eleRef: ElementRef, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; + } ngOnChanges(changes: SimpleChanges): void { this.addDom(this.value, this.term); } @@ -40,16 +45,16 @@ export class HighlightComponent implements OnChanges { const reg = (str) => str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); const regExp = new RegExp('(' + reg(term) + ')', 'gi'); const temp = value.split(regExp); - function createHighLight(text) { - const spanDOM = document.createElement('span'); + const createHighLight = (text) => { + const spanDOM = this.document.createElement('span'); spanDOM.classList.add('devui-match-highlight'); spanDOM.textContent = text; return spanDOM; - } + }; - temp.forEach(function (element, index) { + temp.forEach((element, index) => { if (index % 2 === 0) { - container.appendChild(document.createTextNode(element)); + container.appendChild(this.document.createTextNode(element)); } else { container.appendChild(createHighLight(element)); } diff --git a/devui/utils/lazy-load/lazy-load.directive.ts b/devui/utils/lazy-load/lazy-load.directive.ts index f902fa8b..b14e5482 100644 --- a/devui/utils/lazy-load/lazy-load.directive.ts +++ b/devui/utils/lazy-load/lazy-load.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, SimpleChanges } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges } from '@angular/core'; import { fromEvent, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; @@ -7,6 +7,8 @@ export class LazyLoadDirective implements OnDestroy, OnChanges { // 启用懒加载,默认不启用 @Input() enableLazyLoad = false; + // 滚动监听的目标,默认是宿主, + @Input() target: ElementRef; // 加载更多 @Output() loadMore = new EventEmitter(); @@ -18,9 +20,10 @@ export class LazyLoadDirective implements OnDestroy, OnChanges { constructor(private el: ElementRef) { } ngOnChanges(changes: SimpleChanges): void { + const element = this.target ? this.target : this.el.nativeElement; if (changes && changes['enableLazyLoad']) { if (changes.enableLazyLoad.currentValue) { - this.scrollSubscription = fromEvent(this.el.nativeElement, 'scroll').pipe( + this.scrollSubscription = fromEvent(element, 'scroll').pipe( debounceTime(300), distinctUntilChanged() ).subscribe(event => this.scrollList(event)); @@ -38,7 +41,7 @@ export class LazyLoadDirective implements OnDestroy, OnChanges { } scrollList(event) { - const targetEl = event.target; + const targetEl = event.target.scrollingElement ? event.target.scrollingElement : event.target; const clientHeight = targetEl.clientHeight; const scrollHeight = targetEl.scrollHeight; const scrollTop = targetEl.scrollTop; diff --git a/devui/utils/popper/popper.component.ts b/devui/utils/popper/popper.component.ts index a3f89f79..bf6c19ea 100755 --- a/devui/utils/popper/popper.component.ts +++ b/devui/utils/popper/popper.component.ts @@ -1,9 +1,11 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, + Inject, Input, NgZone, OnDestroy, @@ -63,6 +65,7 @@ export class PopperComponent implements AfterViewInit, OnDestroy { protected popperNode; protected popperParent; private directionSubject = new Subject(); + document: Document; @Output() openChange = new EventEmitter(); @ViewChild('popperActivator', { static: true }) popperActivator: ElementRef; @@ -86,7 +89,8 @@ export class PopperComponent implements AfterViewInit, OnDestroy { } constructor(protected el: ElementRef, protected renderer: Renderer2, protected ngZone: NgZone, - protected changeDetectorRef: ChangeDetectorRef) { + protected changeDetectorRef: ChangeDetectorRef, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } show() { @@ -159,10 +163,10 @@ export class PopperComponent implements AfterViewInit, OnDestroy { setBlurListener() { this.ngZone.runOutsideAngular(() => { if (this.open) { - document.addEventListener('click', this.onDocumentClick); + this.document.addEventListener('click', this.onDocumentClick); this.popperContainer.nativeElement.addEventListener('click', this.blockEvent); } else { - document.removeEventListener('click', this.onDocumentClick); + this.document.removeEventListener('click', this.onDocumentClick); this.popperContainer.nativeElement.removeEventListener('click', this.blockEvent); } }); @@ -298,7 +302,7 @@ export class PopperComponent implements AfterViewInit, OnDestroy { } private attachPopperContainerToSelector(targetSelector) { - const nodeParent = document.querySelector(targetSelector); + const nodeParent = this.document.querySelector(targetSelector); this.attachPopperContainerToNode(nodeParent); } diff --git a/devui/utils/testing/event-helper.ts b/devui/utils/testing/event-helper.ts index f82b4568..a47b0d46 100644 --- a/devui/utils/testing/event-helper.ts +++ b/devui/utils/testing/event-helper.ts @@ -78,6 +78,9 @@ export function createDragEvent(type: dragEventType, params: MouseEventParams) { } export function mouseMoveTrigger(el: HTMLElement, from: { x: number; y: number }, to: { x: number; y: number }): void { + if (typeof window === 'undefined') { + return; + } dispatchMouseEvent(el, 'mousedown', from.x, from.y); dispatchMouseEvent(window.document, 'mousemove', to.x, to.y); dispatchMouseEvent(window.document, 'mouseup'); diff --git a/devui/version.ts b/devui/version.ts index 70dd21a6..f76493da 100755 --- a/devui/version.ts +++ b/devui/version.ts @@ -1,3 +1,3 @@ import { Version } from '@angular/core'; -export const VERSION = new Version('11.3.0'); +export const VERSION = new Version('11.4.0'); diff --git a/devui/window-ref/document-ref.service.ts b/devui/window-ref/document-ref.service.ts index 832d6991..bc7fd621 100755 --- a/devui/window-ref/document-ref.service.ts +++ b/devui/window-ref/document-ref.service.ts @@ -1,16 +1,21 @@ -import { Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; @Injectable() export class DocumentRef { - constructor() { + constructor(@Inject(DOCUMENT) private doc: any) { + } + + get document(): any { + return this.doc; } get body(): any { - return document.body; + return this.document.body; } get documentElement(): any { - return document.documentElement; + return this.document.documentElement; } } diff --git a/devui/window-ref/window-ref.service.ts b/devui/window-ref/window-ref.service.ts index dc0b9473..3629f270 100755 --- a/devui/window-ref/window-ref.service.ts +++ b/devui/window-ref/window-ref.service.ts @@ -4,31 +4,35 @@ import { DocumentRef } from './document-ref.service'; @Injectable() export class WindowRef { - constructor(private documentRef: DocumentRef) { + constructor(private documentRef: DocumentRef) { + } + + get window(): Window | null { + return this.document.defaultView; } get document(): any { - return this.documentRef; + return this.documentRef.document; } get pageXOffset() { - return window.pageXOffset; + return this.window.pageXOffset; } get pageYOffset() { - return window.pageYOffset; + return this.window.pageYOffset; } get innerHeight() { - return window.innerHeight; + return this.window.innerHeight; } get innerWidth() { - return window.innerWidth; + return this.window.innerWidth; } getComputedStyle(element) { - return window.getComputedStyle(element); + return this.window.getComputedStyle(element); } getBoundingClientRect(elementRef: ElementRef) { diff --git a/package.json b/package.json index 61f7a11b..b7680c08 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "ng": "ng", "start": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng serve --open --configuration es5", "start-es6": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng serve --open", - "build": "ng build --prod --deploy-url components/", - "build:separate": "npm run build:devui && ng build --prod --deploy-url components/ --configuration separate && npm run postbuild", + "build": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --prod --deploy-url /components/", + "build:separate": "npm run build:devui && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --prod --deploy-url /components/ --configuration separate", "test": "ng test", "test:lib": "ng test devui-lib --no-watch --no-progress --browsers=ChromeHeadlessCI", "lint": "ng lint", @@ -23,7 +23,6 @@ "prettier": "prettier --config ./.prettierrc --write \"{devui,src}/**/*.html\"", "stylelint": "stylelint \"{devui,src}/**/*.{scss,css}\" --fix", "build:devui": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng build devui-lib --configuration production", - "postbuild": "node scripts/move-assets.js", "postbuild:devui": "node scripts/copy-artifacts.js && npm run css-compile && npm run theme-css-compile && npm run css-postcompile && npm run css-minify && npm run grid-css-compile", "css-compile": "node scripts/sass-handler.js ./devui/style/devui.scss ./publish/devui.css", "css-postcompile": "node scripts/css-postcompile", diff --git a/scripts/es2015-to-es5-bebal-loader/babel-loader-wepack-config.js b/scripts/es2015-to-es5-babel-loader/babel-loader-wepack-config.js similarity index 100% rename from scripts/es2015-to-es5-bebal-loader/babel-loader-wepack-config.js rename to scripts/es2015-to-es5-babel-loader/babel-loader-wepack-config.js diff --git a/scripts/es2015-to-es5-bebal-loader/es6-only-third-party-list.js b/scripts/es2015-to-es5-babel-loader/es6-only-third-party-list.js similarity index 100% rename from scripts/es2015-to-es5-bebal-loader/es6-only-third-party-list.js rename to scripts/es2015-to-es5-babel-loader/es6-only-third-party-list.js diff --git a/scripts/extra-webpack.config.js b/scripts/extra-webpack.config.js index 9e8e1913..7b8612d1 100644 --- a/scripts/extra-webpack.config.js +++ b/scripts/extra-webpack.config.js @@ -2,11 +2,11 @@ const webpackConfigAddTheme = require('./themeable/webpack-config-add-theme'); const sassImporterAlias = require('./sass-importer/webpack-config-sass-loader-importer'); const lessImporterAlias = require('./less-importer/webpack-config-less-loader-importer'); const terserOptionsWebpackConfig = require('./terser-options-webpack.config'); -const webpackConfigAddBabel2ES5 = require('./es2015-to-es5-bebal-loader/babel-loader-wepack-config'); +const webpackConfigAddBabel2ES5 = require('./es2015-to-es5-babel-loader/babel-loader-wepack-config'); module.exports = (config) => { config = sassImporterAlias(config); config = terserOptionsWebpackConfig(config); config = lessImporterAlias(config); config = webpackConfigAddBabel2ES5(config); return webpackConfigAddTheme(config); -}; +}; \ No newline at end of file diff --git a/scripts/move-assets.js b/scripts/move-assets.js deleted file mode 100755 index 6e7189e9..00000000 --- a/scripts/move-assets.js +++ /dev/null @@ -1,3 +0,0 @@ -const shell = require('shelljs'); - -shell.mv('dist/components/index.html','dist/components/favicon.ico', 'dist'); diff --git a/src/app/component/app-content.component.html b/src/app/component/app-content.component.html index b83c9a33..9bd55c34 100755 --- a/src/app/component/app-content.component.html +++ b/src/app/component/app-content.component.html @@ -2,5 +2,5 @@
- +
diff --git a/src/app/component/app-content.component.ts b/src/app/component/app-content.component.ts index 5aa435e7..591ac898 100755 --- a/src/app/component/app-content.component.ts +++ b/src/app/component/app-content.component.ts @@ -10,6 +10,7 @@ import { Subscription } from 'rxjs'; import { ComponentDataService } from './component.data.service'; import { routesConfig } from './component.route'; import { resolveRoutesConfig } from './resolve-routes-config.service'; +import { newScopeList, sunsetScopeList } from './scope-list'; @Component({ selector: 'cd-app-content', // tslint:disable-line templateUrl: './app-content.component.html', @@ -27,13 +28,16 @@ export class AppContentComponent implements OnInit, OnDestroy { clickSub: Subscription = new Subscription(); // @ViewChild('dSearch', { static: true }) dSearch: SearchComponent; componentsDataDisplay = []; + componentsText: any = {}; + overviewText: any = {}; + text: any; constructor(private translate: TranslateService, private comDataService: ComponentDataService) { - this.componentsDataDisplay = resolveRoutesConfig(localStorage.getItem('lang') || 'zh-cn', routesConfig); - this.comDataService.comData = this.componentsDataDisplay; + this.setI18n(); + this.resolveRoutesConfig(); this.generateSideMenuList(localStorage.getItem('lang') || 'zh-cn'); this.translate.onLangChange.subscribe((event: TranslationChangeEvent) => { - this.componentsDataDisplay = resolveRoutesConfig(localStorage.getItem('lang'), routesConfig); - this.comDataService.comData = this.componentsDataDisplay; + this.setI18n(); + this.resolveRoutesConfig(); const values = this.translate.instant('public'); this.generateSideMenuList(values); @@ -42,6 +46,62 @@ export class AppContentComponent implements OnInit, OnDestroy { ngOnInit(): void { } + setI18n() { + this.componentsText = this.translate.instant('components'); + this.overviewText = this.translate.instant('public').overview; + this.text = { + new: this.overviewText.newChange, + sunset: this.overviewText.sunset + }; + } + + setDescription() { + this.componentsDataDisplay.map(componentsGroup => { + componentsGroup.children.map(component => { + const name = component.name.replace(' ', '').toLocaleLowerCase(); + for (const key in this.componentsText) { + if (key.replace(/\-/g, '').toLocaleLowerCase() === name) { + component.description = this.componentsText[key].description.replace(/。/g, ''); + break; + } + } + }); + }); + } + + resolveRoutesConfig() { + this.componentsDataDisplay = resolveRoutesConfig(localStorage.getItem('lang') || 'zh-cn', routesConfig); + + this.setDescription(); + + const newScopes = this.getScopList(newScopeList); + const sunsetScopes = this.getScopList(sunsetScopeList); + this.componentsDataDisplay.map(componentsGroup => { + componentsGroup.children.map(component => { + if (newScopes.includes(component.lowerName)) { + component.newChange = true; + } + if (sunsetScopes.includes(component.lowerName)) { + component.sunset = true; + } + }); + }); + + this.comDataService.comData = this.componentsDataDisplay; + } + + getScopList(list) { + let scopeList; + if (typeof list === 'string') { + scopeList = list.toLocaleLowerCase().match(/\* \*\*.+\:\*\*/g); + } else if (Array.isArray(list)) { + scopeList = list.map(scope => scope.toLocaleLowerCase()); + } else { + scopeList = []; + } + return Array.from(new Set(scopeList.map(scope => scope.replace(/(\W|_|[0-9])*/g, '')))); + } + generateSideMenuList(values) { this.sideMenuList[0].name = values['overview']?.title; this.sideMenuList[1].name = values['start']; diff --git a/src/app/component/component.route.ts b/src/app/component/component.route.ts index a278678d..ce5ebcd1 100755 --- a/src/app/component/component.route.ts +++ b/src/app/component/component.route.ts @@ -261,6 +261,18 @@ export const routesConfig = [ cnName: '公共方法', }, }, + { + path: 'datepickerPro', + component: ExamplePanelComponent, + loadChildren: () => + import('../../../devui/datepicker-pro/demo/datepicker-pro-demo.module').then((m) => m.DatepickerProDemoModule), + data: { + type: '数据录入', + enType: 'Data Entry', + name: 'DatepickerPro', + cnName: '日期选择器', + }, + }, { path: 'datatable', component: ExamplePanelComponent, diff --git a/src/app/component/d-demo-nav.component.ts b/src/app/component/d-demo-nav.component.ts index 40d7261d..aadb9afa 100644 --- a/src/app/component/d-demo-nav.component.ts +++ b/src/app/component/d-demo-nav.component.ts @@ -1,4 +1,5 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, Input, OnInit } from '@angular/core'; @Component({ selector: 'd-demo-nav', @@ -8,9 +9,9 @@ import { Component, Input, OnInit } from '@angular/core'; export class DDemoNavComponent implements OnInit { @Input() navItems: any; demoDocViewerMain; - constructor() {} + constructor(@Inject(DOCUMENT) private doc: any) {} ngOnInit() { - this.demoDocViewerMain = document.querySelector('.doc-viewer-container .main'); + this.demoDocViewerMain = this.doc.querySelector('.doc-viewer-container .main'); } } diff --git a/src/app/component/example-panel.component.html b/src/app/component/example-panel.component.html index 9f8475ba..42dac468 100755 --- a/src/app/component/example-panel.component.html +++ b/src/app/component/example-panel.component.html @@ -2,7 +2,7 @@

{{ componentName }}

{{ description }}
-

{{ 'public.whenToUse' | translate }}

+

{{ 'public.whenToUse' | translate }}

diff --git a/src/app/component/example-panel.component.ts b/src/app/component/example-panel.component.ts index b04730cd..1f126dc2 100755 --- a/src/app/component/example-panel.component.ts +++ b/src/app/component/example-panel.component.ts @@ -1,7 +1,8 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, ComponentFactoryResolver, - ElementRef, Input, + ElementRef, Inject, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; @@ -33,8 +34,11 @@ export class ExamplePanelComponent implements OnInit, AfterViewInit { description: string; tmw: string; componentPath: string; + document: Document; constructor(private componentFactoryResolver: ComponentFactoryResolver, - private router: Router, private route: ActivatedRoute, private translate: TranslateService) { + private router: Router, private route: ActivatedRoute, private translate: TranslateService, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit(): void { @@ -68,7 +72,7 @@ export class ExamplePanelComponent implements OnInit, AfterViewInit { } ngAfterViewInit(): void { - document.body.scrollTop = document.documentElement.scrollTop = 0; + this.document.body.scrollTop = this.document.documentElement.scrollTop = 0; if ((this.typescript.last || {} as any).nativeElement) { hljs.highlightBlock(this.typescript.last.nativeElement); } diff --git a/src/app/component/get-started.component.ts b/src/app/component/get-started.component.ts index 5b9c26f3..a8d9cd1e 100755 --- a/src/app/component/get-started.component.ts +++ b/src/app/component/get-started.component.ts @@ -1,7 +1,9 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, ElementRef, + Inject, Input, OnInit, QueryList, ViewChildren @@ -38,6 +40,7 @@ export class GetStartedComponent implements OnInit, AfterViewInit { this.refreshView(); }); } + document: Document; get readMe () { return this._readMe; @@ -45,7 +48,8 @@ export class GetStartedComponent implements OnInit, AfterViewInit { @ViewChildren('documentation') documentation: QueryList; - constructor(private route: ActivatedRoute, private translate: TranslateService) { + constructor(private route: ActivatedRoute, private translate: TranslateService, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit(): void { const lang = localStorage.getItem('lang'); @@ -61,7 +65,7 @@ export class GetStartedComponent implements OnInit, AfterViewInit { } refreshView() { - Array.from(document.querySelectorAll('pre code')).forEach((block) => { + Array.from(this.document.querySelectorAll('pre code')).forEach((block) => { hljs.highlightBlock(block); }); } diff --git a/src/app/component/getStarted-en.md b/src/app/component/getStarted-en.md index 322e52e1..9423390b 100644 --- a/src/app/component/getStarted-en.md +++ b/src/app/component/getStarted-en.md @@ -27,7 +27,7 @@ Go to your project folder and use npm to install DevUI. ```bash npm i ng-devui # Optional. Font icon library -# npm i devui-assets +# npm i @devui-design/icons ``` ### 3. Import Modules diff --git a/src/app/component/global-config.component.ts b/src/app/component/global-config.component.ts index 23c6cf6c..e987233e 100644 --- a/src/app/component/global-config.component.ts +++ b/src/app/component/global-config.component.ts @@ -1,7 +1,9 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, ElementRef, + Inject, Input, OnInit, QueryList, Renderer2, @@ -40,6 +42,7 @@ export class GlobalConfigComponent implements OnInit, AfterViewInit { this.refreshView(); }); } + document: Document; get readMe () { return this._readMe; @@ -48,7 +51,9 @@ export class GlobalConfigComponent implements OnInit, AfterViewInit { @ViewChildren('documentation') documentation: QueryList; constructor(private router: Router, private route: ActivatedRoute, private translate: TranslateService, - private i18n: I18nService, private elementRef: ElementRef, private renderer: Renderer2) { + private i18n: I18nService, private elementRef: ElementRef, private renderer: Renderer2, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit(): void { const lang = localStorage.getItem('lang'); @@ -64,7 +69,7 @@ export class GlobalConfigComponent implements OnInit, AfterViewInit { } refreshView() { - Array.from(document.querySelectorAll('pre code')).forEach((block) => { + Array.from(this.document.querySelectorAll('pre code')).forEach((block) => { hljs.highlightBlock(block); }); Array.from(this.elementRef.nativeElement.querySelectorAll('a')).forEach((link: HTMLElement) => { @@ -76,6 +81,9 @@ export class GlobalConfigComponent implements OnInit, AfterViewInit { }); } get baseUrl() { + if (typeof window === 'undefined') { + return ''; + } return window.location.pathname.replace(window.location.hash, ''); } diff --git a/src/app/component/overview.component.html b/src/app/component/overview.component.html index 0bc36ea3..96ea0cdb 100644 --- a/src/app/component/overview.component.html +++ b/src/app/component/overview.component.html @@ -10,10 +10,22 @@

{{ overviewText.description }}

+
+
{{ overviewText.filter }}:
+ +

    -
  • - + +
  • {{ components.title }} @@ -26,9 +38,18 @@

    -
    - {{ overviewText.newChange }} -
    + +

    {{ component.title }}

    +

    + {{ component.description }} +

    -
    -

  • + +
- no-data + no-data

@@ -57,15 +81,27 @@

-
- {{ overviewText.newChange }} -
+ +

{{ component.title }}

+

+ {{ component.description }} +

@@ -74,20 +110,38 @@

- {{ overviewText.suggestCmp }} + {{ + nowFilter === 'newChangeCmps' + ? overviewText.newChangeCmps + : nowFilter === 'recentlySuggestCmps' + ? overviewText.recentlySuggestCmps + : overviewText.suggestCmp + }}

-
- {{ overviewText.newChange }} -
+ +

{{ component.title }}

+

+ {{ component.description }} +

diff --git a/src/app/component/overview.component.scss b/src/app/component/overview.component.scss index 3e2f76b6..33da75ad 100644 --- a/src/app/component/overview.component.scss +++ b/src/app/component/overview.component.scss @@ -8,6 +8,7 @@ margin: 32px 0; display: flex; align-items: center; + font-size: $devui-font-size-data-overview; .d-overview-total-components { display: inline-block; @@ -31,6 +32,21 @@ d-search { width: 100%; } + + .d-overview-filters { + margin-top: 18px; + display: flex; + + .d-overview-filters-title { + margin-right: 8px; + font-size: $devui-font-size-card-title; + font-weight: $devui-font-title-weight; + } + + .d-overview-filters-item { + margin-right: 4px; + } + } } .d-overview-components { @@ -43,6 +59,7 @@ h2 { display: inline-block; margin: 0 8px 0 0; + font-size: $devui-font-size-page-title; } .d-overview-component-count { @@ -87,21 +104,23 @@ text-align: center; position: relative; - .d-overview-card-new { + d-tag { position: absolute; top: 0; right: 0; - padding: 0 4px; - color: $devui-warning; - font-size: $devui-font-size; - border-radius: $devui-border-radius; - background-color: $devui-warning-bg; - border: 1px solid $devui-warning-line; + + & + d-tag { + top: 24px; + } + + &.d-overview-shadow:hover { + box-shadow: $devui-shadow-length-connected-overlay $devui-shadow; + } } .d-overview-card-icon { - padding: 20px 0 32px 0; - height: 162px; + padding: 20px 0; + height: 150px; img { width: 110px; @@ -112,13 +131,28 @@ .d-overview-card-title { font-size: $devui-font-size-card-title; line-height: $devui-line-height-base; + color: $devui-text-weak; + font-weight: $devui-font-title-weight; + margin-bottom: 10px; + } + + .d-overview-card-description { color: $devui-aide-text; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } } } .d-overview-no-data { + padding-top: 20px; + + .d-overview-no-data-title { + font-size: $devui-font-size-page-title; + } + .d-overview-no-data-img { transform: translateX(-50%); margin: 20px 0; @@ -126,3 +160,9 @@ } } } + +:host ::ng-deep d-tag { + .devui-tag-item { + cursor: pointer; + } +} diff --git a/src/app/component/overview.component.ts b/src/app/component/overview.component.ts index b57ad689..4807e51c 100644 --- a/src/app/component/overview.component.ts +++ b/src/app/component/overview.component.ts @@ -6,6 +6,7 @@ import { environment } from 'src/environments/environment'; import { ComponentDataService } from './component.data.service'; import { componentMap } from './component.map'; import { filterData } from './resolve-routes-config.service'; +import { suggestScopeList } from './scope-list'; @Component({ selector: 'd-components-overview', @@ -27,48 +28,42 @@ export class ComponentsOverviewComponent implements OnInit, OnDestroy { // 使用14个是因为将页面缩放至最小25%时,一行最多元素为15个,14个刚好可以将只有一个的元素顶到左边界处 flexPlaceHolder = 14; flexPlaceHolders: Array; - - suggestScopeList: Array = [ - 'categorySearch', - 'datePickerPro', - 'gantt', - 'quadrantDiagram', - 'navSprite', - 'dataTable' - ]; - newScopeList: Array | string = [ - 'categorySearch', - 'dataTable', - 'drawer', - 'timeAxis', - 'search', - 'upload', - 'quadrantDiagram', - 'tree', - 'datePicker' + isOpensource = false; + nowFilter: string; + tagList: any = [ + { title: '', name: 'newChangeCmps', checked: false }, + { title: '', name: 'recentlySuggestCmps', checked: false } ]; constructor(private translate: TranslateService, private router: Router, private comDataService: ComponentDataService) { this.comDataService.getComData().subscribe(value => this.componentsData = value); this.componentsDataDisplay = cloneDeep(this.componentsData); - this.overviewText = this.translate.instant('public').overview; + this.setI18n(); + this.translate.onLangChange.subscribe((event: TranslationChangeEvent) => { this.componentsDataDisplay = cloneDeep(this.componentsData); - const values = this.translate.instant('public'); - this.overviewText = values.overview; + this.setI18n(); }); } ngOnInit() { + if (window.location.host === 'devui.design') { + this.isOpensource = true; + } + this.calNumberOfComponents(); this.setPrefix(); - this.initScopeList(); this.setTheme(); - this.setComponentsSuggest(); + this.setComponentsSuggest(suggestScopeList); this.flexPlaceHolders = new Array(this.flexPlaceHolder).fill(0); } + setI18n() { + this.overviewText = this.translate.instant('public').overview; + this.tagList.map(tag => {tag.title = this.overviewText[tag.name]; }); + } + calNumberOfComponents() { this.totalNumComponents = 0; this.componentsData.map(components => { @@ -80,20 +75,8 @@ export class ComponentsOverviewComponent implements OnInit, OnDestroy { this.imgPrefix = './' + this.srcPrefix + '/overview/'; } - initScopeList() { - let scopeList; - if (typeof this.newScopeList === 'string') { - scopeList = this.newScopeList.toLocaleLowerCase().match(/\* \*\*.+\:\*\*/g); - } else if (Array.isArray(this.newScopeList)) { - scopeList = this.newScopeList.map(scope => scope.toLocaleLowerCase()); - } else { - scopeList = []; - } - this.newScopeList = Array.from(new Set(scopeList.map(scope => scope.replace(/(\W|_|[0-9])*/g, '')))); - } - setTheme() { - if (window['devuiThemeService']) { + if (typeof window !== 'undefined' && window['devuiThemeService']) { this.themeService = window['devuiThemeService']; if (window['devuiCurrentTheme']) { this.themeChange(); @@ -104,11 +87,13 @@ export class ComponentsOverviewComponent implements OnInit, OnDestroy { } } - setComponentsSuggest() { + setComponentsSuggest(type) { this.componentsSuggest = []; this.componentsData.map(cmpList => { cmpList.children.map(cmp => { - if (this.suggestScopeList.find(scope => scope.toLocaleLowerCase() === cmp.lowerName)) { + if (Array.isArray(type) && type.find(scope => scope.toLocaleLowerCase() === cmp.lowerName)) { + this.componentsSuggest.push(cloneDeep(cmp)); + } else if (type === 'newChange' && cmp.newChange) { this.componentsSuggest.push(cloneDeep(cmp)); } }); @@ -116,7 +101,7 @@ export class ComponentsOverviewComponent implements OnInit, OnDestroy { } themeChange = () => { - if (window['devuiCurrentTheme'] === 'devui-dark-theme') { + if (typeof window !== 'undefined' && window['devuiCurrentTheme'] === 'devui-dark-theme') { this.darkMode = '-dark'; } else { this.darkMode = ''; @@ -124,6 +109,9 @@ export class ComponentsOverviewComponent implements OnInit, OnDestroy { } searchComponent(event) { + this.nowFilter = undefined; + this.tagList.map(tag => tag.checked = false); + this.setComponentsSuggest(suggestScopeList); this.componentsDataDisplay = filterData(event, this.componentsData); this.componentsLooking = []; if (!this.componentsDataDisplay || !this.componentsDataDisplay.length) { @@ -144,10 +132,36 @@ export class ComponentsOverviewComponent implements OnInit, OnDestroy { } } + filter(type) { + const tagIndex = this.tagList.findIndex(tag => tag.name === type); + this.tagList.map(tag => tag.checked = false); + if (this.nowFilter !== type) { + this.nowFilter = type; + this.tagList[tagIndex].checked = true; + this.componentsDataDisplay = []; + if (type === 'newChangeCmps') { + this.setComponentsSuggest('newChange'); + } else if (type === 'recentlySuggestCmps') { + this.setComponentsSuggest(suggestScopeList); + } + } else { + this.nowFilter = undefined; + this.componentsDataDisplay = cloneDeep(this.componentsData); + this.setComponentsSuggest(suggestScopeList); + } + } + jumpToComponent(link) { this.router.navigate(['components', 'zh-cn', link]); } + jumpToChangeLog(e) { + if (!this.isOpensource) { + e.stopPropagation(); + window.open('http://3ms.huawei.com/hi/group/3945390/wiki_6319622.html', '_blank'); // TODO: 开源版本要改成changelog的链接 + } + } + imgError(event) { const img = event.srcElement; img.src = this.imgPrefix + 'default.png'; diff --git a/src/app/component/resolve-routes-config.service.ts b/src/app/component/resolve-routes-config.service.ts index bedee690..7a47346a 100644 --- a/src/app/component/resolve-routes-config.service.ts +++ b/src/app/component/resolve-routes-config.service.ts @@ -64,7 +64,11 @@ export function resolveRoutesConfig(lang, routesConfig) { export function filterData(event, data) { const res = cloneDeep(data).filter(catalog => { catalog.children = catalog.children.filter(item => { - return item.title.toLowerCase().includes(event.toLowerCase()); + if (typeof event === 'string') { + return item.title.toLowerCase().includes(event.toLowerCase()); + } else if (Array.isArray(event)) { + return event.includes(item.title.toLowerCase()); + } }); return catalog.children.length; }); diff --git a/src/app/component/scope-list.ts b/src/app/component/scope-list.ts new file mode 100644 index 00000000..948e8908 --- /dev/null +++ b/src/app/component/scope-list.ts @@ -0,0 +1,26 @@ +export const suggestScopeList: Array = [ + 'categorySearch', + 'datePickerPro', + 'gantt', + 'quadrantDiagram', + 'navSprite', + 'dataTable' +]; + +export const newScopeList: Array | string = [ + 'categorySearch', + 'trend', + 'datepickerPro', + 'transfer', + 'gantt', + 'timeAxis', + 'tree', + 'treeSelect', + 'upload', + 'cascader', + 'stepsGuide' +]; + +export const sunsetScopeList: Array | string = [ + 'jsmind' +]; diff --git a/src/app/component/theme-guide.component.ts b/src/app/component/theme-guide.component.ts index dc475b5f..1d65beeb 100644 --- a/src/app/component/theme-guide.component.ts +++ b/src/app/component/theme-guide.component.ts @@ -1,7 +1,9 @@ +import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, ElementRef, + Inject, Input, OnInit, QueryList, ViewChildren @@ -39,6 +41,7 @@ import * as hljs from 'highlight.js/lib/core'; this.refreshView(); }); } + document: Document; get readMe () { return this._readMe; @@ -46,7 +49,8 @@ import * as hljs from 'highlight.js/lib/core'; @ViewChildren('documentation') documentation: QueryList; - constructor(private route: ActivatedRoute, private translate: TranslateService) { + constructor(private route: ActivatedRoute, private translate: TranslateService, @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } ngOnInit() { @@ -67,7 +71,7 @@ import * as hljs from 'highlight.js/lib/core'; } refreshView() { - Array.from(document.querySelectorAll('pre code')).forEach((block) => { + Array.from(this.document.querySelectorAll('pre code')).forEach((block) => { hljs.highlightBlock(block); }); } diff --git a/src/app/theme-picker/customize-theme/customize-theme.component.ts b/src/app/theme-picker/customize-theme/customize-theme.component.ts index 276bf0d9..65f19c73 100644 --- a/src/app/theme-picker/customize-theme/customize-theme.component.ts +++ b/src/app/theme-picker/customize-theme/customize-theme.component.ts @@ -1,3 +1,4 @@ +import { DOCUMENT } from '@angular/common'; import { Component, Inject, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { DEVUI_LANG } from 'ng-devui/i18n'; @@ -10,9 +11,12 @@ import { CustomThemeService } from './custom-theme.service'; styleUrls: ['./customize-theme.component.scss'], }) export class CustomizeThemeComponent implements OnInit { - constructor(private cts: CustomThemeService, - private route: Router, @Inject(DEVUI_LANG) private appLang) { + document: Document; + constructor(private cts: CustomThemeService, + private route: Router, @Inject(DEVUI_LANG) private appLang, + @Inject(DOCUMENT) private doc: any) { + this.document = this.doc; } themeService: ThemeService; customDark = false; @@ -21,8 +25,10 @@ export class CustomizeThemeComponent implements OnInit { refreshSignal: boolean; ngOnInit() { - this.themeService = window['devuiThemeService']; - this.getCustomConfigColor(); + if (typeof window !== 'undefined') { + this.themeService = window['devuiThemeService']; + this.getCustomConfigColor(); + } } customApplyTheme() { this.themeService.applyTheme(new Theme({ @@ -91,18 +97,21 @@ export const myTheme: Theme = new Theme({ return buf; } downloadFileFromArrayBuffer = (data: ArrayBuffer, filename: string, contentType: string) => { + if (typeof window === 'undefined') { + return; + } if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE11 support const blob = new Blob([data], { type: contentType }); window.navigator.msSaveOrOpenBlob(blob, filename); } else {// other browsers - if ('download' in document.createElement('a')) { + if ('download' in this.document.createElement('a')) { const blob = new Blob([data], { type: contentType }); - const link = document.createElement('a'); + const link = this.document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; - document.body.appendChild(link); + this.document.body.appendChild(link); link.click(); - document.body.removeChild(link); + this.document.body.removeChild(link); URL.revokeObjectURL(link.href); } else { // not support tag a download attribute use file download, filename won't support diff --git a/src/app/theme-picker/theme-data-more.ts b/src/app/theme-picker/theme-data-more.ts index 77e15fee..d2593021 100644 --- a/src/app/theme-picker/theme-data-more.ts +++ b/src/app/theme-picker/theme-data-more.ts @@ -89,86 +89,23 @@ export const greenDarkTheme: Theme = new Theme({ extends: 'devui-dark-theme', }); -export const devuiLightLargeTheme: Theme = new Theme({ - id: 'devui-light-large-theme', - name: 'Light Large Mode', - cnName: '浅色大字号主题', - data: Object.assign({}, devuiLightTheme.data, { - 'devui-font-size': '14px', - 'devui-font-size-card-title': '16px', - 'devui-font-size-page-title': '18px', - 'devui-font-size-modal-title': '20px', - 'devui-font-size-price': '22px', - 'devui-font-size-data-overview': '26px', - - 'devui-font-size-icon': '18px', - 'devui-font-size-sm': '14px', - 'devui-font-size-md': '14px', - 'devui-font-size-lg': '16px', - }), - isDark: false, - extends: 'devui-light-theme', -}); - -export const devuiDarkLargeTheme: Theme = new Theme({ - id: 'devui-dark-large-theme', - name: 'Dark Large Mode', - cnName: '深色大字号主题', - data: Object.assign({}, devuiDarkTheme.data, { - 'devui-font-size': '14px', - 'devui-font-size-card-title': '16px', - 'devui-font-size-page-title': '18px', - 'devui-font-size-modal-title': '20px', - 'devui-font-size-price': '22px', - 'devui-font-size-data-overview': '26px', - - 'devui-font-size-icon': '18px', - 'devui-font-size-sm': '14px', - 'devui-font-size-md': '14px', - 'devui-font-size-lg': '16px', - }), - isDark: true, - extends: 'devui-dark-theme', -}); - -export const greenLightLargeTheme: Theme = new Theme({ - id: 'green-light-large-theme', - name: 'Green Light Large Mode', - cnName: '绿浅色大字号主题', - data: Object.assign({}, greenLightTheme.data, { - 'devui-font-size': '14px', - 'devui-font-size-card-title': '16px', - 'devui-font-size-page-title': '18px', - 'devui-font-size-modal-title': '20px', - 'devui-font-size-price': '22px', - 'devui-font-size-data-overview': '26px', - - 'devui-font-size-icon': '18px', - 'devui-font-size-sm': '14px', - 'devui-font-size-md': '14px', - 'devui-font-size-lg': '16px', - }), - isDark: true, - extends: 'devui-light-theme', +export const devuiLargeFontTheme: Theme = new Theme({ + id: 'devui-large-font-theme', + name: 'Large Font Mode', + cnName: '大字号主题', + data: {} }); -export const greenDarkLargeTheme: Theme = new Theme({ - id: 'green-dark-large-theme', - name: 'Green Dark Large Mode', - cnName: '绿深色大字号主题', - data: Object.assign({}, greenDarkTheme.data, { - 'devui-font-size': '14px', - 'devui-font-size-card-title': '16px', - 'devui-font-size-page-title': '18px', - 'devui-font-size-modal-title': '20px', - 'devui-font-size-price': '22px', - 'devui-font-size-data-overview': '26px', +export const LargeFontSize = { + 'devui-font-size': '14px', + 'devui-font-size-card-title': '16px', + 'devui-font-size-page-title': '18px', + 'devui-font-size-modal-title': '20px', + 'devui-font-size-price': '22px', + 'devui-font-size-data-overview': '26px', - 'devui-font-size-icon': '18px', - 'devui-font-size-sm': '14px', - 'devui-font-size-md': '14px', - 'devui-font-size-lg': '16px', - }), - isDark: true, - extends: 'devui-dark-theme', -}); + 'devui-font-size-icon': '18px', + 'devui-font-size-sm': '14px', + 'devui-font-size-md': '14px', + 'devui-font-size-lg': '16px', +}; diff --git a/src/app/theme-picker/theme-picker.component.html b/src/app/theme-picker/theme-picker.component.html index 89d0cc9b..27b390b4 100644 --- a/src/app/theme-picker/theme-picker.component.html +++ b/src/app/theme-picker/theme-picker.component.html @@ -62,7 +62,7 @@ {{ 'themePicker.follow' | translate }}
- + {{ 'themePicker.largeSize' | translate }}
diff --git a/src/app/theme-picker/theme-picker.component.ts b/src/app/theme-picker/theme-picker.component.ts index 5343ba14..8f933b7a 100644 --- a/src/app/theme-picker/theme-picker.component.ts +++ b/src/app/theme-picker/theme-picker.component.ts @@ -4,7 +4,7 @@ import { Subscription } from 'rxjs'; import { environment } from 'src/environments/environment'; import { CustomThemeService } from './customize-theme/custom-theme.service'; import { createTheme } from './customize-theme/util'; -// import { greenLightTheme } from './theme-data-more'; +import { LargeFontSize } from './theme-data-more'; @Component({ selector: 'app-theme-picker', @@ -15,13 +15,13 @@ export class ThemePickerComponent implements OnInit, OnDestroy { themeService: ThemeService; themes; theme: string; - curTheme: Theme; + largeFontTheme: Theme; fontSize: 'normal' | 'large' = 'normal'; themeMode: 'light' | 'dark' = 'light'; themePrefix: 'devui' | 'green' | string = 'devui'; themePrefersColorScheme: boolean; sub: Subscription; - protectEye = false; + largeFontSizeMode = false; activeThemeType = 'devuiTheme'; advancedThemeList = [{ value: 'infinity', url: 'assets/infinity.png' }, { value: 'sweet', url: 'assets/sweet.png' }, @@ -29,32 +29,32 @@ export class ThemePickerComponent implements OnInit, OnDestroy { { value: 'deep', url: 'assets/deep.png' }, { value: 'galaxy', url: 'assets/galaxy.png' }]; currentAdvancedTheme = 'infinity'; - assetsPrefix = './'; + assetsPrefix = environment.deployPrefix; constructor( private cdr: ChangeDetectorRef, private cts: CustomThemeService ) { } ngOnInit() { - if (environment.production) { - this.assetsPrefix = './components/'; + if (typeof window !== 'undefined') { + this.themeService = window['devuiThemeService']; + this.themes = window['devuiThemes']; + this.theme = window['devuiCurrentTheme']; } - this.themeService = window['devuiThemeService']; - this.themes = window['devuiThemes']; - this.theme = window['devuiCurrentTheme']; this.themePrefix = this.getThemePrefix(); this.themeMode = this.themes[this.theme]?.isDark ? 'dark' : 'light'; - this.protectEye = this.theme.split('-')[2] === 'large' ? true : false; + this.largeFontSizeMode = this.theme === 'devui-large-font-theme'; + this.largeFontTheme = this.themes['devui-large-font-theme']; this.themePrefersColorScheme = this.getThemePrefersColorSchemeOn(); + this.initTheme(); this.cdr.detectChanges(); if (this.themePrefersColorScheme) { this.themePrefersColorSchemeChange(true); } - this.initTheme(); } getThemePrefix() { - return (this.theme.split('-')[0] !== 'devui' || this.theme.split('-')[0] !== 'green') ? 'devui' : this.theme.split('-')[0]; + return (this.theme.split('-')[0] !== 'devui' && this.theme.split('-')[0] !== 'green') ? 'devui' : this.theme.split('-')[0]; } initTheme() { if (this.isCustomizeTheme()) { @@ -94,8 +94,9 @@ export class ThemePickerComponent implements OnInit, OnDestroy { } themesChange() { - if (this.protectEye) { - this.theme = `${this.themePrefix}-${this.themeMode}-large-theme`; + if (this.largeFontSizeMode) { + this.largeFontTheme.data = Object.assign({}, this.themes[`${this.themePrefix}-${this.themeMode}-theme`].data, LargeFontSize); + this.theme = `devui-large-font-theme`; } else { this.theme = `${this.themePrefix}-${this.themeMode}-theme`; } @@ -109,7 +110,13 @@ export class ThemePickerComponent implements OnInit, OnDestroy { } themeFontSizeChange() { - this.themesChange(); + if (typeof window !== 'undefined' && this.largeFontSizeMode) { + this.largeFontTheme.data = Object.assign({}, this.themes[window['devuiCurrentTheme']].data, LargeFontSize); + this.theme = `devui-large-font-theme`; + } else { + this.theme = `${this.themePrefix}-${this.themeMode}-theme`; + } + this.themeService.applyTheme(this.themes[this.theme]); } themeFontSizeSchemeChange(event: boolean) { diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index 1e3b0d0f..5c075d6a 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -31,8 +31,12 @@ "description": "DevUI provides rich basic UI components for web applications. We will continue to explore the best UI practices of enterprise-level applications. Welcome to DevUI.", "placeholder": "Enter the component", "newChange": "New", + "sunset": "Sunset", "lookingCmp": "Is the components you're looking for?", - "suggestCmp": "Recommended Components" + "suggestCmp": "Recommended Components", + "filter": "Filter", + "newChangeCmps": "Components with new changes", + "recentlySuggestCmps": "Recently suggest components" }, "start": "Getting Started", "themeDoc": "Customize Theme Docs", @@ -826,7 +830,9 @@ "tmw": "Can be used to deal with date string conversion, file download, a tag simulation, and lazy loading.", "lazyLoadDemo": { "title": "Lazyload Directive", - "description": "Import the LazyLoadModule in the util module. The dLazyLoad instruction is used to respond to the loadMore event when the container is rolled to the bottom to implement lazy loading." + "description": "Import the LazyLoadModule in the util module. The dLazyLoad instruction is used to respond to the loadMore event when the container is rolled to the bottom to implement lazy loading.", + "p1": "Default Rolling Listening Host", + "p2": "Scroll monitoring uses other elements, such as window." }, "pipeDemo": { "title": "DatePipe" @@ -1984,9 +1990,14 @@ "manualDestroy": "Manual Destroy", "closeManualDestroy": "If this function is disabled, it can be manually destroyed. If this function is enabled, it is invalid." }, + "templateDemo": { + "title": "Custom Template", + "description": "Custom Drawer Template." + }, "anchorLinkValues": { "basic-usage": "Basic Usage", - "do-not-destroy-after-closing": "Do not destroy after being closed" + "do-not-destroy-after-closing": "Do not destroy after being closed", + "template": "Custom Template" } }, "dropdown": { @@ -3687,9 +3698,13 @@ "title": "Resizable", "placeholder": "Please enter the content." }, + "countDemo": { + "title": "Listening input and maximum number of words" + }, "anchorLinkValues": { "basic-usage": "Basic Usage", - "resize": "Resizable" + "resize": "Resizable", + "count": "Listening input and maximum number of words" } }, "steps-guide": { @@ -3857,6 +3872,20 @@ "datepicker-pro-static-panel": "Calender Panel Mode", "select-type": "Service scenario: date selector of the select type" } + }, + "trend": { + "name": "Trend", + "type": "Data Enter", + "path": "trend", + "description": "Digital display with trends", + "tmw": "Displays individual data and its trend.", + "basicDemo": { + "title": "Basic Usage", + "description": "" + }, + "anchorLinkValues": { + "basicDemo": "Basic Usage" + } } } } \ No newline at end of file diff --git a/src/assets/i18n/zh-cn.json b/src/assets/i18n/zh-cn.json index 0b89107e..c3164b07 100644 --- a/src/assets/i18n/zh-cn.json +++ b/src/assets/i18n/zh-cn.json @@ -31,8 +31,12 @@ "description": "DevUI 为 Web 应用提供了丰富的基础 UI 组件,我们还将持续探索企业级应用的最佳 UI 实践,欢迎尝试使用 DevUI。", "placeholder": "请输入你想查找的组件", "newChange": "有更新", + "sunset": "日落", "lookingCmp": "您要找的组件是不是?", - "suggestCmp": "推荐组件" + "suggestCmp": "推荐组件", + "filter": "过滤器", + "newChangeCmps": "有更新的", + "recentlySuggestCmps": "近期推荐" }, "start": "快速开始", "themeDoc": "主题化使用指南", @@ -819,7 +823,9 @@ "tmw": "处理日期字符串转换、文件下载、a标签模拟、懒加载等。", "lazyLoadDemo": { "title": "懒加载指令", - "description": "引入util模块中的LazyLoadModule,使用dLazyLoad指令在滚到到容器底部时响应loadMore事件实现懒加载。" + "description": "引入util模块中的LazyLoadModule,使用dLazyLoad指令在滚到到容器底部时响应loadMore事件实现懒加载。", + "p1": "默认滚动监听使用宿主", + "p2": "滚动监听使用其他元素,比如window·" }, "pipeDemo": { "title": "日期解析器" @@ -1977,9 +1983,14 @@ "manualDestroy": "手动销毁", "closeManualDestroy": "关闭后可手动销毁,打开状态调用无效" }, + "templateDemo": { + "title": "自定义模板", + "description": "自定义抽屉板模板。" + }, "anchorLinkValues": { "basic-usage": "基本用法", - "do-not-destroy-after-closing": "关闭后不销毁" + "do-not-destroy-after-closing": "关闭后不销毁", + "template": "自定义模板" } }, "dropdown": { @@ -2771,13 +2782,17 @@ "select": "请选择", "custom": "自定义" }, + "header-templateDemo": { + "title": "下拉头模板" + }, "anchorLinkValues": { "basic-usage": "基本用法", "multiple-cascader": "多选类型", "search-cascader": "搜索类型", "parent-cascader": "父级可选", "template-cascader": "模板类型", - "lazyload-cascader": "点击加载" + "lazyload-cascader": "点击加载", + "cascader-header-template": "下拉头模板" } }, "status": { @@ -3412,7 +3427,7 @@ "description": "允许传入正则或正则字符串限制输入,输入时会优先匹配传入的正则,不输入则不限制。" }, "decimalLimitDemo": { - "title": "限制小数和自动获取焦点", + "title": "限制小数", "Limit Decimal": "限制小数" }, "anchorLinkValues": { @@ -3678,9 +3693,13 @@ "title": "调整大小", "placeholder": "请输入内容" }, + "countDemo": { + "title": "监听输入及最大字数" + }, "anchorLinkValues": { "basic-usage": "基本用法", - "resize": "调整大小" + "resize": "调整大小", + "count": "监听输入及最大字数" } }, "steps-guide": { @@ -3848,6 +3867,23 @@ "datepicker-pro-static-panel": "日历面板模式", "select-type": "业务场景:select类型日期选择器" } + }, + "trend": { + "name": "Trend 趋势", + "type": "数据录入", + "path": "trend", + "description": "带趋势的数字展示", + "tmw": "展示单个数据及其趋势。", + "basicDemo": { + "title": "基本用法" + }, + "trend-icon-template": { + "title": "自定义趋势图标" + }, + "anchorLinkValues": { + "basicDemo": "基本用法", + "trend-icon-template": "自定义趋势图标" + } } } } \ No newline at end of file diff --git a/src/assets/overview/data-entry/datepickerpro.png b/src/assets/overview/data-entry/datepickerpro.png new file mode 100644 index 0000000000000000000000000000000000000000..2b60885802d2f72571f54116ce20f8a7561c428f GIT binary patch literal 6355 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 literal 0 HcmV?d00001 diff --git a/src/main.ts b/src/main.ts index 1da88011..82ff1aeb 100755 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,7 @@ import { } from 'ng-devui/theme'; import { deepTheme, galaxyTheme, infinityTheme, provenceTheme, sweetTheme } from 'ng-devui/theme-collection'; import { AppModule } from './app/app.module'; -import { - devuiDarkLargeTheme, devuiLightLargeTheme, - greenDarkLargeTheme, greenDarkTheme, - greenLightLargeTheme, greenLightTheme -} from './app/theme-picker/theme-data-more'; +import { devuiLargeFontTheme, greenDarkTheme, greenLightTheme } from './app/theme-picker/theme-data-more'; import { environment } from './environments/environment'; ThemeServiceInit({ @@ -18,10 +14,7 @@ ThemeServiceInit({ 'devui-dark-theme': devuiDarkTheme, 'green-light-theme': greenLightTheme, 'green-dark-theme': greenDarkTheme, - 'devui-light-large-theme': devuiLightLargeTheme, - 'devui-dark-large-theme': devuiDarkLargeTheme, - 'green-light-large-theme': greenLightLargeTheme, - 'green-dark-large-theme': greenDarkLargeTheme, + 'devui-large-font-theme': devuiLargeFontTheme, 'infinity-theme': infinityTheme, 'provence-theme': provenceTheme, 'sweet-theme': sweetTheme, diff --git a/src/styles.scss b/src/styles.scss index eda4e550..62608af6 100755 --- a/src/styles.scss +++ b/src/styles.scss @@ -40,7 +40,7 @@ pre { word-break: break-all; word-wrap: break-word; border: 1px solid $devui-dividing-line; - border-radius: 1px; + border-radius: $devui-border-radius; } .hljs { @@ -145,7 +145,7 @@ code { // font-size: 90%; color: #c7254e; background-color: $devui-area; - border-radius: 1px; + border-radius: $devui-border-radius; } .readme { @@ -186,3 +186,7 @@ code { [dTextInput]::-ms-reveal { display: none; } + +.devui-h3-title { + font-size: $devui-font-size-card-title; +}